Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Certificate.load to load certificate chain. #441

Merged
merged 22 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions ext/openssl/ossl_bio.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,31 @@ ossl_obj2bio(volatile VALUE *pobj)
BIO *bio;

if (RB_TYPE_P(obj, T_FILE))
obj = rb_funcallv(obj, rb_intern("read"), 0, NULL);
obj = rb_funcallv(obj, rb_intern("read"), 0, NULL);

StringValue(obj);
bio = BIO_new_mem_buf(RSTRING_PTR(obj), RSTRING_LENINT(obj));

if (!bio)
ossl_raise(eOSSLError, "BIO_new_mem_buf");
ossl_raise(eOSSLError, "BIO_new_mem_buf");

*pobj = obj;
return bio;
}

VALUE
ossl_membio2str(BIO *bio)
{
VALUE ret;
VALUE result;
int state;
BUF_MEM *buf;
BUF_MEM *memory_buffer;

BIO_get_mem_ptr(bio, &buf);
ret = ossl_str_new(buf->data, buf->length, &state);
BIO_get_mem_ptr(bio, &memory_buffer);
result = ossl_str_new(memory_buffer->data, memory_buffer->length, &state);
BIO_free(bio);

if (state)
rb_jump_tag(state);
rb_jump_tag(state);

return ret;
return result;
}
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.
*/
ioquatix marked this conversation as resolved.
Show resolved Hide resolved
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
ioquatix marked this conversation as resolved.
Show resolved Hide resolved
end

class CRL
Expand Down
Empty file.
Empty file.
Binary file added test/openssl/fixtures/pkey/fullchain.der
Binary file not shown.
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-----
ioquatix marked this conversation as resolved.
Show resolved Hide resolved
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
ioquatix marked this conversation as resolved.
Show resolved Hide resolved

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
ioquatix marked this conversation as resolved.
Show resolved Hide resolved
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_fullchain_der
fullchain_path = Fixtures.file_path("pkey", "fullchain.der")
certificates = OpenSSL::X509::Certificate.load_file(fullchain_path)
ioquatix marked this conversation as resolved.
Show resolved Hide resolved

# 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