Skip to content

Commit

Permalink
Implement Certificate.load to load certificate chain. (#441)
Browse files Browse the repository at this point in the history
* Add feature for loading the chained certificate into Certificate array.

Co-authored-by: Sao I Kuan <saoikuan@gmail.com>
  • Loading branch information
ioquatix and sikuan committed May 21, 2021
1 parent 964d836 commit 05e1c01
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 0 deletions.
153 changes: 153 additions & 0 deletions ext/openssl/ossl_x509cert.c
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,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
*/
Expand Down Expand Up @@ -812,6 +963,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);
Expand Down
4 changes: 4 additions & 0 deletions lib/openssl/x509.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added test/openssl/fixtures/pkey/certificate.der
Binary file not shown.
Empty file.
Empty file.
56 changes: 56 additions & 0 deletions test/openssl/fixtures/pkey/fullchain.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIIFKTCCBBGgAwIBAgISBFspP+tJfRaC6xprreB4Rp9KMA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yMTA0MTcwMjQzMTlaFw0yMTA3MTYwMjQzMTlaMBwxGjAYBgNVBAMT
EXd3dy5jb2Rlb3Rha3UuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAx6h5vNPfkkrtYWxn1PWDDLRAwrGmZbkYPttjHBRSwTcd7rsIX4PcSzw9fWxm
K4vIkAYoKAElIvsSE3xRUjyzMrACfdhK5J8rG25fq94iVyoYaNBQV0WMJkO6X47s
hGeIKkK91ohR5b2tMw3/z9zELP0TVo2TPG7rYsBZm34myldqDA8yVEBEOa+Qdpda
9xewPhkkdpAU55qgWTrD21m7vGq9WpsBz4wNKnwVsaugtkRH82VPIfaL4ZI9kox6
QoPWe/tHUBdlDkuT7ud77eLAWnC/5Clg28/9GU/Z8Nj8SrrKuXL6WUXmxxaAhWUR
Qx4VblZeuIpwd0nHyP0hz4CWKQIDAQABo4ICTTCCAkkwDgYDVR0PAQH/BAQDAgWg
MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0G
A1UdDgQWBBTKiSGZuLFSIG2JPbFSZa9TxMu5WTAfBgNVHSMEGDAWgBQULrMXt1hW
y65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6
Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iu
b3JnLzAcBgNVHREEFTATghF3d3cuY29kZW90YWt1LmNvbTBMBgNVHSAERTBDMAgG
BmeBDAECATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3Bz
LmxldHNlbmNyeXB0Lm9yZzCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3AJQgvB6O
1Y1siHMfgosiLA3R2k1ebE+UPWHbTi9YTaLCAAABeN3s/lgAAAQDAEgwRgIhAKFY
Q+vBe3zyeBazxp8kVN7oLvcQ6Y9PPz199tVhYnEbAiEAhU/xdbQaY/6b93h+7NTF
sPG7X4lq/3UoNgoXcAVGZgoAdgD2XJQv0XcwIhRUGAgwlFaO400TGTO/3wwvIAvM
TvFk4wAAAXjd7P5OAAAEAwBHMEUCIQDWd79+jWaGuf3acm5/yV95jL2KvzeGFfdU
HZlKIeWFmAIgDSZ6ug7AyhYNKjzFV4ZSICln+L4yI92EpOa+8gDG6/0wDQYJKoZI
hvcNAQELBQADggEBAHIhMYm06lLFmJL+cfIg5fFEmFNdHmmZn88Hypv4/MtmqTKv
5asF/z3TvhW4hX2+TY+NdcqGT7cZFo/ZF/tS6oBXPgmBYM1dEfp2FAdnGNOySC5Y
7RC4Uk9TUpP2g101YBmj6dQKQluAwIQk+gO4MSlHE0J0U/lMpjvrLWcuHbV4/xWJ
IdM+iPq8GeYt5epYmNc7XeRIgv7V3RxDQdBv2OVM5mtPVerdiO0ISrdbe5mvz2+Z
rhSg+EJNHlmMwcq5HqtMwS8M8Ax+vLmWCOkPWXhyV8wQaQcFjZJfpIGUvCnMTqsh
kSIYXq2CbSDUUFRFssNN6EdVms0KnmW3BUu0xAk=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEZTCCA02gAwIBAgIQQAF1BIMUpMghjISpDBbN3zANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTIwMTAwNzE5MjE0MFoXDTIxMDkyOTE5MjE0MFow
MjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxCzAJBgNVBAMT
AlIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuwIVKMz2oJTTDxLs
jVWSw/iC8ZmmekKIp10mqrUrucVMsa+Oa/l1yKPXD0eUFFU1V4yeqKI5GfWCPEKp
Tm71O8Mu243AsFzzWTjn7c9p8FoLG77AlCQlh/o3cbMT5xys4Zvv2+Q7RVJFlqnB
U840yFLuta7tj95gcOKlVKu2bQ6XpUA0ayvTvGbrZjR8+muLj1cpmfgwF126cm/7
gcWt0oZYPRfH5wm78Sv3htzB2nFd1EbjzK0lwYi8YGd1ZrPxGPeiXOZT/zqItkel
/xMY6pgJdz+dU/nPAeX1pnAXFK9jpP+Zs5Od3FOnBv5IhR2haa4ldbsTzFID9e1R
oYvbFQIDAQABo4IBaDCCAWQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E
BAMCAYYwSwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5p
ZGVudHJ1c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTE
p7Gkeyxx+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEE
AYLfEwEBATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2Vu
Y3J5cHQub3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0
LmNvbS9EU1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYf
r52LFMLGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0B
AQsFAAOCAQEA2UzgyfWEiDcx27sT4rP8i2tiEmxYt0l+PAK3qB8oYevO4C5z70kH
ejWEHx2taPDY/laBL21/WKZuNTYQHHPD5b1tXgHXbnL7KqC401dk5VvCadTQsvd8
S8MXjohyc9z9/G2948kLjmE6Flh9dDYrVYA9x2O+hEPGOaEOa1eePynBgPayvUfL
qjBstzLhWVQLGAkXXmNs+5ZnPBxzDJOLxhF2JIbeQAcH5H0tZrUlo5ZYyOqA7s9p
O5b85o3AM/OJ+CktFBQtfvBhcJVd9wvlwPsk+uyOy2HI7mNxKKgsBTt375teA2Tw
UdHkhVNcsAKX1H7GNNLOEADksd86wuoXvg==
-----END CERTIFICATE-----
1 change: 1 addition & 0 deletions test/openssl/fixtures/pkey/garbage.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World
32 changes: 32 additions & 0 deletions test/openssl/test_x509cert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,38 @@ def test_marshal
assert_equal cert.to_der, deserialized.to_der
end

def test_load_file_empty_pem
empty_path = Fixtures.file_path("pkey", "empty.pem")
assert_raise(OpenSSL::X509::CertificateError) do
OpenSSL::X509::Certificate.load_file(empty_path)
end
end

def test_load_file_fullchain_pem
fullchain_path = Fixtures.file_path("pkey", "fullchain.pem")
certificates = OpenSSL::X509::Certificate.load_file(fullchain_path)
assert_equal 2, certificates.size
assert_equal "/CN=www.codeotaku.com", certificates[0].subject.to_s
assert_equal "/C=US/O=Let's Encrypt/CN=R3", certificates[1].subject.to_s
end

def test_load_file_certificate_der
fullchain_path = Fixtures.file_path("pkey", "certificate.der")
certificates = OpenSSL::X509::Certificate.load_file(fullchain_path)

# DER encoding can only contain one certificate:
assert_equal 1, certificates.size
assert_equal "/CN=www.codeotaku.com", certificates[0].subject.to_s
end

def test_load_file_fullchain_garbage
fullchain_path = Fixtures.file_path("pkey", "garbage.txt")

assert_raise(OpenSSL::X509::CertificateError) do
certificates = OpenSSL::X509::Certificate.load_file(fullchain_path)
end
end

private

def certificate_error_returns_false
Expand Down

0 comments on commit 05e1c01

Please sign in to comment.