diff --git a/ext/openssl/ossl_x509crl.c b/ext/openssl/ossl_x509crl.c index 9b59bda9e..722eb819e 100644 --- a/ext/openssl/ossl_x509crl.c +++ b/ext/openssl/ossl_x509crl.c @@ -297,6 +297,44 @@ ossl_x509crl_get_revoked(VALUE self) return ary; } +/* + * call-seq: + * crl.by_serial(serial) -> OpenSSL::X509::Revoked or nil + * + * Looks up the certificate _serial_ (an Integer or OpenSSL::BN) in the CRL and + * returns the matching OpenSSL::X509::Revoked entry, or +nil+ if that serial is + * not listed. + * + * Unlike iterating over #revoked, this does not instantiate the entire + * revocation list: it performs a sorted lookup (wrapping the OpenSSL function + * +X509_CRL_get0_by_serial+), which is significantly faster and uses far less + * memory for large CRLs. + * + * crl.by_serial(cert.serial) #=> # or nil + * crl.by_serial(cert.serial)&.time #=> revocation time, if revoked + */ +static VALUE +ossl_x509crl_by_serial(VALUE self, VALUE serial) +{ + X509_CRL *crl; + X509_REVOKED *rev = NULL; + ASN1_INTEGER *asn1_serial; + int found; + + GetX509CRL(self, crl); + asn1_serial = num_to_asn1integer(serial, NULL); + + /* 0 = not found, 1 = found, 2 = found with reason removeFromCRL */ + found = X509_CRL_get0_by_serial(crl, &rev, asn1_serial); + ASN1_INTEGER_free(asn1_serial); + + if (found == 0) + return Qnil; + + /* ossl_x509revoked_new dups, so the result outlives the CRL safely */ + return ossl_x509revoked_new(rev); +} + static VALUE ossl_x509crl_set_revoked(VALUE self, VALUE ary) { @@ -525,6 +563,7 @@ Init_ossl_x509crl(void) rb_define_method(cX509CRL, "next_update=", ossl_x509crl_set_next_update, 1); rb_define_method(cX509CRL, "revoked", ossl_x509crl_get_revoked, 0); rb_define_method(cX509CRL, "revoked=", ossl_x509crl_set_revoked, 1); + rb_define_method(cX509CRL, "by_serial", ossl_x509crl_by_serial, 1); rb_define_method(cX509CRL, "add_revoked", ossl_x509crl_add_revoked, 1); rb_define_method(cX509CRL, "sign", ossl_x509crl_sign, 2); rb_define_method(cX509CRL, "verify", ossl_x509crl_verify, 1); diff --git a/test/openssl/test_x509crl.rb b/test/openssl/test_x509crl.rb index 81c9247df..a827f069e 100644 --- a/test/openssl/test_x509crl.rb +++ b/test/openssl/test_x509crl.rb @@ -105,6 +105,44 @@ def test_revoked assert_equal(revoked.map(&:serial), revoked2.map(&:serial)) end + def test_by_serial + now = Time.at(Time.now.to_i) + revoke_info = [ + [1, Time.at(0), 1], # keyCompromise + [2, Time.at(0x7fffffff), 2], # cACompromise + [3, now, 3], # affiliationChanged + ] + cert = issue_cert(@ca, @rsa1, 1, [], nil, nil) + crl = issue_crl(revoke_info, 1, Time.now, Time.now+1600, [], + cert, @rsa1, "SHA256") + + # Returns the matching Revoked entry for a listed serial + rev = crl.by_serial(2) + assert_instance_of(OpenSSL::X509::Revoked, rev) + assert_equal(2, rev.serial) + assert_equal(Time.at(0x7fffffff), rev.time) + assert_equal("CRLReason", rev.extensions[0].oid) + assert_equal("CA Compromise", rev.extensions[0].value) + + # Accepts an OpenSSL::BN as well as an Integer + assert_equal(3, crl.by_serial(OpenSSL::BN.new(3)).serial) + + # Returns nil for a serial that is not listed + assert_nil(crl.by_serial(42)) + + # Same answers after a DER round-trip, and consistent with #revoked + crl = OpenSSL::X509::CRL.new(crl.to_der) + revoke_info.each do |serial, time, _reason| + by_serial = crl.by_serial(serial) + from_list = crl.revoked.find {|r| r.serial == serial } + assert_equal(by_serial, from_list) + assert_equal(time, by_serial.time) + end + + # Rejects values that can't be an integer serial + assert_raise(TypeError) { crl.by_serial(nil) } + end + def test_extension cert_exts = [ ["basicConstraints", "CA:TRUE", true],