diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 8aac52ef4..f53915f02 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -143,6 +143,9 @@ def find_openssl_library # added in 1.1.0, currently not in LibreSSL have_func("EVP_PBE_scrypt(\"\", 0, (unsigned char *)\"\", 0, 0, 0, 0, 0, NULL, 0)", evp_h) +# look for CMS code, normally included, but some variations compile it out +have_func("CMS_sign", ssl_h) + # added in OpenSSL 1.1.1 and LibreSSL 3.5.0, then removed in LibreSSL 4.0.0 have_func("EVP_PKEY_check(NULL)", evp_h) diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 60780790b..dae85fcac 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -1035,6 +1035,7 @@ Init_openssl(void) Init_ossl_asn1(); Init_ossl_bn(); Init_ossl_cipher(); + Init_ossl_cms(); Init_ossl_config(); Init_ossl_digest(); Init_ossl_engine(); diff --git a/ext/openssl/ossl.h b/ext/openssl/ossl.h index 22471d208..d40158b82 100644 --- a/ext/openssl/ossl.h +++ b/ext/openssl/ossl.h @@ -25,6 +25,7 @@ #include +#include #include #include #include @@ -185,6 +186,7 @@ extern VALUE dOSSL; #include "ossl_bn.h" #include "ossl_cipher.h" #include "ossl_config.h" +#include "ossl_cms.h" #include "ossl_digest.h" #include "ossl_engine.h" #include "ossl_hmac.h" diff --git a/ext/openssl/ossl_cms.c b/ext/openssl/ossl_cms.c new file mode 100644 index 000000000..1c8913c51 --- /dev/null +++ b/ext/openssl/ossl_cms.c @@ -0,0 +1,522 @@ +/* + * 'OpenSSL for Ruby' project + * Copyright (C) 2018 Michael Richardson + * copied from ossl_pkcs7.c: + * Copyright (C) 2001-2002 Michal Rokos + * All rights reserved. + */ +/* + * This program is licensed under the same licence as Ruby. + * (See the file 'LICENCE'.) + */ +#include "ossl.h" + +#if !defined(OPENSSL_NO_CMS) +/* + * The CMS_ContentInfo is the primary data structure which this module creates and maintains + * Is is called OpenSSL::CMS::ContentInfo in ruby. + * + */ + +#define NewCMSContentInfo(klass) \ + TypedData_Wrap_Struct((klass), &ossl_cms_content_info_type, 0) +#define SetCMSContentInfo(obj, cmsci) do { \ + if (!(cmsci)) { \ + ossl_raise(rb_eRuntimeError, "CMS wasn't initialized."); \ + } \ + RTYPEDDATA_DATA(obj) = (cmsci); \ +} while (0) +#define GetCMSContentInfo(obj, cmsci) do { \ + TypedData_Get_Struct((obj), CMS_ContentInfo, &ossl_cms_content_info_type, (cmsci)); \ + if (!(cmsci)) { \ + ossl_raise(rb_eRuntimeError, "CMS wasn't initialized."); \ + } \ +} while (0) + +#define NewCMSsi(klass) \ + TypedData_Wrap_Struct((klass), &ossl_cms_signer_info_type, 0) +#define SetCMSsi(obj, cmssi) do { \ + if (!(cmssi)) { \ + ossl_raise(rb_eRuntimeError, "CMSsi wasn't initialized."); \ + } \ + RTYPEDDATA_DATA(obj) = (cmssi); \ +} while (0) +#define GetCMSsi(obj, cmssi) do { \ + TypedData_Get_Struct((obj), CMS_SignerInfo, &ossl_cms_signer_info_type, (cmssi)); \ + if (!(cmssi)) { \ + ossl_raise(rb_eRuntimeError, "CMSsi wasn't initialized."); \ + } \ +} while (0) + + +#define ossl_cmsci_set_data(o,v) rb_iv_set((o), "@data", (v)) +#define ossl_cmsci_get_data(o) rb_iv_get((o), "@data") +#define ossl_cmsci_set_err_string(o,v) rb_iv_set((o), "@error_string", (v)) +#define ossl_cmsci_get_err_string(o) rb_iv_get((o), "@error_string") + +static VALUE cCMS; +static VALUE cCMSContentInfo; +static VALUE cCMSSignerInfo; +#if 0 +/* not yet implemented. */ +static VALUE cCMSRecipient; +#endif +static VALUE eCMSError; + + +static void +ossl_cms_content_info_free(void *ptr) +{ + CMS_ContentInfo_free(ptr); +} + +static const rb_data_type_t ossl_cms_content_info_type = { + "OpenSSL/CMS/ContentInfo", + { + 0, ossl_cms_content_info_free, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, +}; + +static void +ossl_cms_signer_info_free(void *ptr) +{ + /* nothing, only internal pointers are ever returned */ +} + +static const rb_data_type_t ossl_cms_signer_info_type = { + "OpenSSL/CMS/SignerInfo", + { + 0, ossl_cms_signer_info_free, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, +}; + + + +static VALUE +ossl_cmsci_to_pem(VALUE self) +{ + CMS_ContentInfo *cmsci; + BIO *out; + VALUE str; + + GetCMSContentInfo(self, cmsci); + if (!(out = BIO_new(BIO_s_mem()))) { + ossl_raise(eCMSError, NULL); + } + if (!PEM_write_bio_CMS(out, cmsci)) { + BIO_free(out); + ossl_raise(eCMSError, NULL); + } + str = ossl_membio2str(out); + + return str; +} + +/* + * call-seq: + * cmsci.to_der => binary + */ +static VALUE +ossl_cmsci_to_der(VALUE self) +{ + CMS_ContentInfo *cmsci; + VALUE str; + long len; + unsigned char *p; + + GetCMSContentInfo(self, cmsci); + if((len = i2d_CMS_ContentInfo(cmsci, NULL)) <= 0) + ossl_raise(eCMSError, NULL); + str = rb_str_new(0, len); + p = (unsigned char *)RSTRING_PTR(str); + if(i2d_CMS_ContentInfo(cmsci, &p) <= 0) + ossl_raise(eCMSError, NULL); + ossl_str_adjust(str, p); + + return str; +} + + +static VALUE +ossl_cmsci_alloc(VALUE klass) +{ + CMS_ContentInfo *cms; + VALUE obj; + + obj = NewCMSContentInfo(klass); + if (!(cms = CMS_ContentInfo_new())) { + ossl_raise(eCMSError, NULL); + } + SetCMSContentInfo(obj, cms); + + return obj; +} + +/* + * call-seq: + * CMS::ContentInfo.new => cmsci + * CMS::ContentInfo.new(string) => cmsi + * + * Create a new ContentInfo object. With argument decode from PEM or DER + * format CMS object. + * + */ +static VALUE +ossl_cmsci_initialize(int argc, VALUE *argv, VALUE self) +{ + CMS_ContentInfo *c1, *cms = DATA_PTR(self); + BIO *in; + VALUE arg; + + //GetCMSContentInfo(self, cms); + if(rb_scan_args(argc, argv, "01", &arg) == 0) + return self; + arg = ossl_to_der_if_possible(arg); + in = ossl_obj2bio(&arg); + c1 = PEM_read_bio_CMS(in, &cms, NULL, NULL); + if (!c1) { + OSSL_BIO_reset(in); + c1 = d2i_CMS_bio(in, &cms); + if (!c1) { + BIO_free(in); + CMS_ContentInfo_free(cms); + DATA_PTR(self) = NULL; + ossl_raise(rb_eArgError, "Could not parse the CMS"); + } + } + SetCMSContentInfo(self, cms); + BIO_free(in); + ossl_cmsci_set_data(self, Qnil); + ossl_cmsci_set_err_string(self, Qnil); + + return self; +} + +static VALUE +ossl_cmsci_verify(int argc, VALUE *argv, VALUE self) +{ + VALUE certs, store, indata, flags; + STACK_OF(X509) *x509s; + X509_STORE *x509st; + int flg, ok, status = 0; + BIO *in, *out; + CMS_ContentInfo *cmsci; + VALUE data; + const char *msg; + + GetCMSContentInfo(self, cmsci); + rb_scan_args(argc, argv, "22", &certs, &store, &indata, &flags); + x509st = GetX509StorePtr(store); + flg = NIL_P(flags) ? 0 : NUM2INT(flags); + if(NIL_P(indata)) indata = ossl_cmsci_get_data(self); + in = NIL_P(indata) ? NULL : ossl_obj2bio(&indata); + if(NIL_P(certs)) x509s = NULL; + else{ + x509s = ossl_protect_x509_ary2sk(certs, &status); + if(status){ + BIO_free(in); + rb_jump_tag(status); + } + } + if(!(out = BIO_new(BIO_s_mem()))){ + BIO_free(in); + sk_X509_pop_free(x509s, X509_free); + ossl_raise(eCMSError, NULL); + } + ok = CMS_verify(cmsci, x509s, x509st, in, out, flg); + BIO_free(in); + sk_X509_pop_free(x509s, X509_free); + if (ok < 0) ossl_raise(eCMSError, "CMS_verify"); + msg = ERR_reason_error_string(ERR_peek_error()); + ossl_cmsci_set_err_string(self, msg ? rb_str_new2(msg) : Qnil); + ossl_clear_error(); + data = ossl_membio2str(out); + ossl_cmsci_set_data(self, data); + + return (ok == 1) ? Qtrue : Qfalse; +} + + +static STACK_OF(X509) * +cmsci_get_certs(VALUE self) +{ + CMS_ContentInfo *cms; + STACK_OF(X509) *certs; + + GetCMSContentInfo(self, cms); + certs = CMS_get1_certs(cms); + return certs; +} + +static VALUE +ossl_cmsci_add_certificate(VALUE self, VALUE cert) +{ + CMS_ContentInfo *cms; + X509 *x509; + + GetCMSContentInfo(self, cms); + x509 = GetX509CertPtr(cert); /* NO NEED TO DUP */ + if (!CMS_add1_cert(cms, x509)){ /* add1() takes reference */ + ossl_raise(eCMSError, NULL); + } + + return self; +} + +static VALUE +ossl_cmsci_set_certs_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, arg)) +{ + return ossl_cmsci_add_certificate(arg, i); +} + +static VALUE +ossl_cmsci_set_certificates(VALUE self, VALUE ary) +{ + STACK_OF(X509) *certs; + X509 *cert; + + certs = cmsci_get_certs(self); + while((cert = sk_X509_pop(certs))) X509_free(cert); + rb_block_call(ary, rb_intern("each"), 0, 0, ossl_cmsci_set_certs_i, self); + + return ary; +} + +static VALUE +ossl_cmsci_get_certificates(VALUE self) +{ + return ossl_x509_sk2ary(cmsci_get_certs(self)); +} + +/* + * CMS SignerInfo is not a first class object, but part of the + * CMS ContentInfo. It can be wrapped in a ruby object, but it can + * not be created or freed directly. + */ +static VALUE +ossl_cmssi_new(CMS_SignerInfo *cmssi) +{ + VALUE obj; + + obj = NewCMSsi(cCMSSignerInfo); + SetCMSsi(obj, cmssi); + rb_ivar_set(obj, rb_intern("cms"), cmssi); + + return obj; +} + +static VALUE +ossl_cmssi_get_issuer(VALUE self) +{ + CMS_SignerInfo *cmssi; + ASN1_OCTET_STRING *keyid; + X509_NAME *issuer; + ASN1_INTEGER *sno; + + GetCMSsi(self, cmssi); + + if(CMS_SignerInfo_get0_signer_id(cmssi,&keyid,&issuer, &sno)!=1) { + ossl_raise(eCMSError, "get0_signer_id failed"); + } + + /* XXX keyid may be set instead */ + if(issuer) { + return ossl_x509name_new(issuer); + } else { + return Qnil; + } +} + +static VALUE +ossl_cmssi_get_serial(VALUE self) +{ + CMS_SignerInfo *cmssi; + ASN1_OCTET_STRING *keyid; + X509_NAME *issuer; + ASN1_INTEGER *sno; + + GetCMSsi(self, cmssi); + + if(CMS_SignerInfo_get0_signer_id(cmssi,&keyid,&issuer, &sno)!=1) { + ossl_raise(eCMSError, "get0_signer_id failed"); + } + + /* XXX keyid may be set */ + if(sno) { + return asn1integer_to_num(sno); + } else { + return Qnil; + } +} + +static VALUE +ossl_cmsci_get_signers(VALUE self) +{ + CMS_ContentInfo *cms; + STACK_OF(CMS_SignerInfo) *sk; + int num, i; + VALUE ary; + + GetCMSContentInfo(self, cms); + if (!(sk = CMS_get0_SignerInfos(cms))) { + return rb_ary_new(); + } + num = sk_CMS_SignerInfo_num(sk); + ary = rb_ary_new2(num); + for (i=0; i cms + * + * CMS.sign creates and returns a CMS SignedData structure. + * The data will be signed with *key* (An OpenSSL::PKey instance), and the list of + * certs (if any) will be included in the structure as additional + * anchors. + * + * The flags come from the set of XYZ. + * + */ +static VALUE +ossl_cms_s_sign(int argc, VALUE *argv, VALUE klass) +{ + VALUE cert, key, data, certs, flags; + X509 *x509; + EVP_PKEY *pkey; + BIO *in; + STACK_OF(X509) *x509s; + int flg, status = 0; + CMS_ContentInfo *cms_cinfo; + VALUE ret; + + x509 = NULL; + pkey = NULL; + in = NULL; + rb_scan_args(argc, argv, "32", &cert, &key, &data, &certs, &flags); + if(!NIL_P(cert)) { + x509 = GetX509CertPtr(cert); /* NO NEED TO DUP */ + } + if(!NIL_P(key)) { + pkey = GetPrivPKeyPtr(key); /* NO NEED TO DUP */ + } + flg = NIL_P(flags) ? 0 : NUM2INT(flags); + ret = NewCMSContentInfo(cCMSContentInfo); + if(!NIL_P(data)) { + in = ossl_obj2bio(&data); + } + if(NIL_P(certs)) x509s = NULL; + else{ + x509s = ossl_protect_x509_ary2sk(certs, &status); + if(status){ + BIO_free(in); + rb_jump_tag(status); + } + } + if(!(cms_cinfo = CMS_sign(x509, pkey, x509s, in, flg))){ + BIO_free(in); + sk_X509_pop_free(x509s, X509_free); + ossl_raise(eCMSError, NULL); + } + SetCMSContentInfo(ret, cms_cinfo); + ossl_cmsci_set_data(ret, data); + ossl_cmsci_set_err_string(ret, Qnil); + BIO_free(in); + sk_X509_pop_free(x509s, X509_free); + + return ret; +} + +/* + * INIT CMS interface + */ +void +Init_ossl_cms(void) +{ + cCMS = rb_define_class_under(mOSSL, "CMS", rb_cObject); + rb_define_singleton_method(cCMS, "sign", ossl_cms_s_sign, -1); + + eCMSError = rb_define_class_under(cCMS, "CMSError", eOSSLError); + + cCMSContentInfo = rb_define_class_under(cCMS, "ContentInfo", rb_cObject); + rb_define_alloc_func(cCMSContentInfo, ossl_cmsci_alloc); + + rb_define_method(cCMSContentInfo, "to_pem", ossl_cmsci_to_pem, 0); + rb_define_alias(cCMSContentInfo, "to_s", "to_pem"); + rb_define_method(cCMSContentInfo, "to_der", ossl_cmsci_to_der, 0); + rb_define_method(cCMSContentInfo, "initialize", ossl_cmsci_initialize, -1); + + rb_define_method(cCMSContentInfo, "certificates=", ossl_cmsci_set_certificates, 1); + rb_define_method(cCMSContentInfo, "certificates", ossl_cmsci_get_certificates, 0); + rb_define_method(cCMSContentInfo, "signers", ossl_cmsci_get_signers, 0); + rb_define_method(cCMSContentInfo, "verify", ossl_cmsci_verify, -1); + rb_attr(cCMSContentInfo, rb_intern("data"), 1, 0, Qfalse); + rb_attr(cCMSContentInfo, rb_intern("error_string"), 1, 1, Qfalse); +#if 0 + rb_define_method(cCMSContentInfo, "add_signer", ossl_cmsci_add_signer, 1); +#endif + + cCMSSignerInfo = rb_define_class_under(cCMS, "SignerInfo", rb_cObject); + rb_undef_alloc_func(cCMSSignerInfo); + rb_define_method(cCMSSignerInfo,"issuer", ossl_cmssi_get_issuer, 0); + rb_define_alias(cCMSSignerInfo, "name", "issuer"); + rb_define_method(cCMSSignerInfo,"serial", ossl_cmssi_get_serial,0); + +#if 0 + /* not yet implemented. */ + rb_define_singleton_method(cCMS, "read_smime", ossl_cms_s_read_smime, 1); + rb_define_singleton_method(cCMS, "write_smime", ossl_cms_s_write_smime, -1); + rb_define_singleton_method(cCMS, "encrypt", ossl_cms_s_encrypt, -1); + rb_define_method(cCMS, "initialize_copy", ossl_cms_copy, 1); + rb_define_method(cCMS, "detached=", ossl_cms_set_detached, 1); + rb_define_method(cCMS, "detached", ossl_cms_get_detached, 0); + rb_define_method(cCMS, "detached?", ossl_cms_detached_p, 0); + rb_define_method(cCMS, "cipher=", ossl_cms_set_cipher, 1); + rb_define_method(cCMS, "add_recipient", ossl_cms_add_recipient, 1); + rb_define_method(cCMS, "recipients", ossl_cms_get_recipient, 0); + rb_define_method(cCMS, "add_certificate", ossl_cms_add_certificate, 1); + rb_define_method(cCMS, "add_crl", ossl_cms_add_crl, 1); + rb_define_method(cCMS, "crls=", ossl_cms_set_crls, 1); + rb_define_method(cCMS, "crls", ossl_cms_get_crls, 0); + rb_define_method(cCMS, "add_data", ossl_cms_add_data, 1); + rb_define_alias(cCMS, "data=", "add_data"); + rb_define_method(cCMS, "decrypt", ossl_cms_decrypt, -1); + + cCMSRecipient = rb_define_class_under(cCMS,"RecipientInfo",rb_cObject); + rb_define_alloc_func(cCMSRecipient, ossl_cmsri_alloc); + rb_define_method(cCMSRecipient, "initialize", ossl_cmsri_initialize,1); + rb_define_method(cCMSRecipient, "issuer", ossl_cmsri_get_issuer,0); + rb_define_method(cCMSRecipient, "serial", ossl_cmsri_get_serial,0); + rb_define_method(cCMSRecipient, "enc_key", ossl_cmsri_get_enc_key,0); +#endif + +#define DefCMSConst(x) rb_define_const(cCMS, #x, INT2NUM(CMS_##x)) + + DefCMSConst(NO_SIGNER_CERT_VERIFY); + DefCMSConst(NOINTERN); + DefCMSConst(TEXT); + DefCMSConst(NOCERTS); + DefCMSConst(DETACHED); + DefCMSConst(BINARY); + DefCMSConst(NOATTR); + DefCMSConst(NOSMIMECAP); + DefCMSConst(USE_KEYID); + DefCMSConst(STREAM); + DefCMSConst(PARTIAL); +} + +#else +/* empty init function for when OPENSSL_NO_CMS */ +void Init_ossl_cms(void) +{ + /* nothing */ +} + +#endif /* OPENSSL_NO_CMS */ diff --git a/ext/openssl/ossl_cms.h b/ext/openssl/ossl_cms.h new file mode 100644 index 000000000..23fc30d82 --- /dev/null +++ b/ext/openssl/ossl_cms.h @@ -0,0 +1,15 @@ +/* + * 'OpenSSL for Ruby' project + * Copyright (C) 2001-2002 Michal Rokos + * All rights reserved. + */ +/* + * This program is licensed under the same licence as Ruby. + * (See the file 'LICENCE'.) + */ +#if !defined(_OSSL_CMS_H_) +#define _OSSL_CMS_H_ + +void Init_ossl_cms(void); + +#endif /* _OSSL_CMS_H_ */ diff --git a/test/openssl/test_cms.rb b/test/openssl/test_cms.rb new file mode 100644 index 000000000..2b6d3a30e --- /dev/null +++ b/test/openssl/test_cms.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: false +require_relative 'utils' + +if defined?(OpenSSL::CMS) + +class OpenSSL::TestCMS < OpenSSL::TestCase + def setup + super + @rsa1024 = Fixtures.pkey("rsa1024") + @rsa2048 = Fixtures.pkey("rsa2048") + ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA") + ee1 = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=EE1") + ee2 = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=EE2") + + ca_exts = [ + ["basicConstraints","CA:TRUE",true], + ["keyUsage","keyCertSign, cRLSign",true], + ["subjectKeyIdentifier","hash",false], + ["authorityKeyIdentifier","keyid:always",false], + ] + @ca_cert = issue_cert(ca, @rsa2048, 1, ca_exts, nil, nil) + ee_exts = [ + ["keyUsage","Non Repudiation, Digital Signature, Key Encipherment",true], + ["authorityKeyIdentifier","keyid:always",false], + ["extendedKeyUsage","clientAuth, emailProtection, codeSigning",false], + ] + @ee1_cert = issue_cert(ee1, @rsa1024, 2, ee_exts, @ca_cert, @rsa2048) + @ee2_cert = issue_cert(ee2, @rsa1024, 3, ee_exts, @ca_cert, @rsa2048) + end + + def test_signed + # cms.signers does not produce FIPS compliant things (not sure why) + omit_on_fips + + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + ca_certs = [@ca_cert] + + data = "aaaaa\r\nbbbbb\r\nccccc\r\n" + tmp = OpenSSL::CMS.sign(@ee1_cert, @rsa1024, data, ca_certs) + cms = OpenSSL::CMS::ContentInfo.new(tmp.to_der) + certs = cms.certificates + signers = cms.signers + assert(cms.verify([], store)) + assert_equal(data, cms.data) + assert_equal(2, certs.size) + assert_equal(@ee1_cert.subject.to_s, certs[0].subject.to_s) + assert_equal(@ca_cert.subject.to_s, certs[1].subject.to_s) + assert_equal(1, signers.size) + assert_equal(@ee1_cert.serial, signers[0].serial) + assert_equal(@ee1_cert.issuer.to_s, signers[0].issuer.to_s) + + # Normally OpenSSL tries to translate the supplied content into canonical + # MIME format (e.g. a newline character is converted into CR+LF). + # If the content is a binary, CMS::BINARY flag should be used. + + data = "aaaaa\nbbbbb\nccccc\n" + flag = OpenSSL::CMS::BINARY + tmp = OpenSSL::CMS.sign(@ee1_cert, @rsa1024, data, ca_certs, flag) + cms = OpenSSL::CMS::ContentInfo.new(tmp.to_der) + certs = cms.certificates + signers = cms.signers + assert(cms.verify([], store)) + assert_equal(data, cms.data) + assert_equal(2, certs.size) + assert_equal(@ee1_cert.subject.to_s, certs[0].subject.to_s) + assert_equal(@ca_cert.subject.to_s, certs[1].subject.to_s) + assert_equal(1, signers.size) + assert_equal(@ee1_cert.serial, signers[0].serial) + assert_equal(@ee1_cert.issuer.to_s, signers[0].issuer.to_s) + + if false + # multiple signers not yet supported. + # A signed-data which have multiple signatures can be created + # through the following steps. + # 1. create two signed-data + # 2. copy signerInfo and certificate from one to another + + tmp1 = OpenSSL::CMS.sign(@ee1_cert, @rsa1024, data, [], flag) + tmp2 = OpenSSL::CMS.sign(@ee2_cert, @rsa1024, data, [], flag) + tmp1.add_signer(tmp2.signers[0]) + tmp1.add_certificate(@ee2_cert) + + cms = OpenSSL::CMS.ContentInfo.new(tmp1.to_der) + certs = cms.certificates + signers = cms.signers + assert(cms.verify([], store)) + assert_equal(data, cms.data) + assert_equal(2, certs.size) + assert_equal(2, signers.size) + assert_equal(@ee1_cert.serial, signers[0].serial) + assert_equal(@ee1_cert.issuer.to_s, signers[0].issuer.to_s) + assert_equal(@ee2_cert.serial, signers[1].serial) + assert_equal(@ee2_cert.issuer.to_s, signers[1].issuer.to_s) + end + end + +end +end # if(OpenSSL)