From 1146a94aeea7d2ea1ead3bfcbddd0f4de696abe6 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 22 May 2021 08:47:20 +1200 Subject: [PATCH] [ruby/openssl] Implement `Certificate.load` to load certificate chain. (https://github.com/ruby/openssl/pull/441) * Add feature for loading the chained certificate into Certificate array. https://github.com/ruby/openssl/commit/05e1c015d6 Co-authored-by: Sao I Kuan --- ext/openssl/lib/openssl/x509.rb | 4 + ext/openssl/ossl_x509cert.c | 153 +++++++++++++++++++++ test/openssl/fixtures/pkey/certificate.der | Bin 0 -> 1325 bytes test/openssl/fixtures/pkey/empty.der | 0 test/openssl/fixtures/pkey/empty.pem | 0 test/openssl/fixtures/pkey/fullchain.pem | 56 ++++++++ test/openssl/fixtures/pkey/garbage.txt | 1 + test/openssl/test_x509cert.rb | 32 +++++ 8 files changed, 246 insertions(+) create mode 100644 test/openssl/fixtures/pkey/certificate.der create mode 100644 test/openssl/fixtures/pkey/empty.der create mode 100644 test/openssl/fixtures/pkey/empty.pem create mode 100644 test/openssl/fixtures/pkey/fullchain.pem create mode 100644 test/openssl/fixtures/pkey/garbage.txt diff --git a/ext/openssl/lib/openssl/x509.rb b/ext/openssl/lib/openssl/x509.rb index 6771b90c1a5bbf..367a899e21e1a2 100644 --- a/ext/openssl/lib/openssl/x509.rb +++ b/ext/openssl/lib/openssl/x509.rb @@ -338,6 +338,10 @@ def pretty_print(q) q.text 'not_after='; q.pp self.not_after } end + + def self.load_file(path) + load(File.binread(path)) + end end class CRL diff --git a/ext/openssl/ossl_x509cert.c b/ext/openssl/ossl_x509cert.c index 1385411069f63a..4884fc89a0d9e6 100644 --- a/ext/openssl/ossl_x509cert.c +++ b/ext/openssl/ossl_x509cert.c @@ -707,6 +707,157 @@ ossl_x509_eq(VALUE self, VALUE other) return !X509_cmp(a, b) ? Qtrue : Qfalse; } +struct load_chained_certificates_arguments { + VALUE certificates; + X509 *certificate; +}; + +static VALUE +load_chained_certificates_append_push(VALUE _arguments) { + struct load_chained_certificates_arguments *arguments = (struct load_chained_certificates_arguments*)_arguments; + + if (arguments->certificates == Qnil) { + arguments->certificates = rb_ary_new(); + } + + rb_ary_push(arguments->certificates, ossl_x509_new(arguments->certificate)); + + return Qnil; +} + +static VALUE +load_chained_certificate_append_ensure(VALUE _arguments) { + struct load_chained_certificates_arguments *arguments = (struct load_chained_certificates_arguments*)_arguments; + + X509_free(arguments->certificate); + + return Qnil; +} + +inline static VALUE +load_chained_certificates_append(VALUE certificates, X509 *certificate) { + struct load_chained_certificates_arguments arguments; + arguments.certificates = certificates; + arguments.certificate = certificate; + + rb_ensure(load_chained_certificates_append_push, (VALUE)&arguments, load_chained_certificate_append_ensure, (VALUE)&arguments); + + return arguments.certificates; +} + +static VALUE +load_chained_certificates_PEM(BIO *in) { + VALUE certificates = Qnil; + X509 *certificate = PEM_read_bio_X509(in, NULL, NULL, NULL); + + /* If we cannot read even one certificate: */ + if (certificate == NULL) { + /* If we cannot read one certificate because we could not read the PEM encoding: */ + if (ERR_GET_REASON(ERR_peek_last_error()) == PEM_R_NO_START_LINE) { + ossl_clear_error(); + } + + if (ERR_peek_last_error()) + ossl_raise(eX509CertError, NULL); + else + return Qnil; + } + + certificates = load_chained_certificates_append(Qnil, certificate); + + while ((certificate = PEM_read_bio_X509(in, NULL, NULL, NULL))) { + load_chained_certificates_append(certificates, certificate); + } + + /* We tried to read one more certificate but could not read start line: */ + if (ERR_GET_REASON(ERR_peek_last_error()) == PEM_R_NO_START_LINE) { + /* This is not an error, it means we are finished: */ + ossl_clear_error(); + + return certificates; + } + + /* Alternatively, if we reached the end of the file and there was no error: */ + if (BIO_eof(in) && !ERR_peek_last_error()) { + return certificates; + } else { + /* Otherwise, we tried to read a certificate but failed somewhere: */ + ossl_raise(eX509CertError, NULL); + } +} + +static VALUE +load_chained_certificates_DER(BIO *in) { + X509 *certificate = d2i_X509_bio(in, NULL); + + /* If we cannot read one certificate: */ + if (certificate == NULL) { + /* Ignore error. We could not load. */ + ossl_clear_error(); + + return Qnil; + } + + return load_chained_certificates_append(Qnil, certificate); +} + +static VALUE +load_chained_certificates(VALUE _io) { + BIO *in = (BIO*)_io; + VALUE certificates = Qnil; + + /* + DER is a binary format and it may contain octets within it that look like + PEM encoded certificates. So we need to check DER first. + */ + certificates = load_chained_certificates_DER(in); + + if (certificates != Qnil) + return certificates; + + OSSL_BIO_reset(in); + + certificates = load_chained_certificates_PEM(in); + + if (certificates != Qnil) + return certificates; + + /* Otherwise we couldn't read the output correctly so fail: */ + ossl_raise(eX509CertError, "Could not detect format of certificate data!"); +} + +static VALUE +load_chained_certificates_ensure(VALUE _io) { + BIO *in = (BIO*)_io; + + BIO_free(in); + + return Qnil; +} + +/* + * call-seq: + * OpenSSL::X509::Certificate.load(string) -> [certs...] + * OpenSSL::X509::Certificate.load(file) -> [certs...] + * + * Read the chained certificates from the given input. Supports both PEM + * and DER encoded certificates. + * + * PEM is a text format and supports more than one certificate. + * + * DER is a binary format and only supports one certificate. + * + * If the file is empty, or contains only unrelated data, an + * +OpenSSL::X509::CertificateError+ exception will be raised. + */ +static VALUE +ossl_x509_load(VALUE klass, VALUE buffer) +{ + BIO *in = ossl_obj2bio(&buffer); + + return rb_ensure(load_chained_certificates, (VALUE)in, load_chained_certificates_ensure, (VALUE)in); +} + /* * INIT */ @@ -815,6 +966,8 @@ Init_ossl_x509cert(void) */ cX509Cert = rb_define_class_under(mX509, "Certificate", rb_cObject); + rb_define_singleton_method(cX509Cert, "load", ossl_x509_load, 1); + rb_define_alloc_func(cX509Cert, ossl_x509_alloc); rb_define_method(cX509Cert, "initialize", ossl_x509_initialize, -1); rb_define_method(cX509Cert, "initialize_copy", ossl_x509_copy, 1); diff --git a/test/openssl/fixtures/pkey/certificate.der b/test/openssl/fixtures/pkey/certificate.der new file mode 100644 index 0000000000000000000000000000000000000000..7d44df8413aac2a7b71767f5926cb70c12bf77f2 GIT binary patch literal 1325 zcmXqLV%0QgVi8=x%*4pVB*YS}Y5&@@R;=l@RQB2j6>jsr40zc%wc0$|zVk9NaqoMbzx zr^>1MTJ>-D0P$2l@5%3;SHFF9AgW;h6U~I%=l@Fj-~4dnkJqkKJBxlrx;{HD*3gB7N;gn~D`9FALnz5aDA6~N0og0aB5EMqge86WfMgX| zTn(HJIM~?I8+jO+7!AzXxPiW9VQjh&^cw44LcK*FplvjT; zzOrX;*M9Rh(eEL$Grbg_w$5Nu;8m;I#dqM87_XMi(eSn)1)<0d% z`rr2QKG7YmwflVgdrMEX`EQpL>%S@fJLpwIxE*I>!r55CUu7cf=@OSWdFVyF71$Bv z9~?CKTXU>wg5=_tSGb&_8xFLn{9@i=sOc{3R1*BtXPNbD-E=+Ktrh=8I~6b6b^O{R z`AqletB4ubtz(~fH2u4JSH{`#LjH}XKF_lKLtowPc+26nJ-T}4`t$iS*NH6n;p8h9 z*>mvJPPx@S2laVA@YL