Inconsistency between cert: and cert_pem: parameters to ssl_bind
copiousfreetime opened this issue · comments
Describe the bug
When using ssl_bind
the documentation seems to indicate that the following are equivalent:
File based certs/keys
Lines 513 to 520 in 763d1a1
String based certs/keys
Lines 526 to 531 in 763d1a1
This appears to be incorrect.
When cert:
is passed to ssl_bind
- inside MiniSSL this file is loaded from disk using SSL_CTX_use_certificate_chain_file()
SSL_CTX_use_certificate_chain_file() loads a certificate chain from file into ctx. The certificates must be in PEM format and must be sorted starting with the subject's certificate (actual client or server certificate), followed by intermediate CA certificates if applicable, and ending at the highest level (root) CA
puma/ext/puma_http11/mini_ssl.c
Lines 272 to 278 in 763d1a1
When cert_pem:
is passed to ssl_bind
- inside MiniSSL this file is loaded using a combination of PEM_read_bio_X509
and SSL_CTX_use_certificate
puma/ext/puma_http11/mini_ssl.c
Lines 300 to 311 in 763d1a1
If the contents of the file at path_to_cert
is a full chain PEM then:
- passing
cert: path_to_cert
a fully resolve cert chain is served up by puma - passing in
cert_pem: File.read(path_to_cert)
results in only the certificate itelf being loaded, and not the full chain.
To Reproduce
Take the self contained gist puma_ssl_test.rb
and run it with ruby:
curl -s -O https://gist.githubusercontent.com/copiousfreetime/8ad7784ea609b2c5f6e5a3ccf0c9a2d6/raw/4c0197fafbf24fe2bf10438e873752b60e44c4ed/puma_ssl_test.rb
ruby puma_ssl_test.rb --help
There are 3 different tests
ruby puma_ssl_test.rb --test files
ruby puma_ssl_test.rb --test strings
ruby puma_ssl_test.rb --test ca
Expected Behavior
I would expect that give the same contents to cert_pem:
that was in the file that was given to cert:
should result in the same level of ssl validation.
ruby puma_ssl_test.rb --test files
% ruby puma_ssl_test.rb --test files
Writing pem/cert.pem
Writing pem/key.pem
Writing pem/ca.pem
Writing pem/all.pem
Test puma with ssl_bind options:
cert: pem/all.pem
key: pem/key.pem
The cert file contains the certificate and the entire cert chain
Once Puma is running, you can test it with:
echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
Puma starting in single mode...
* Puma version: 6.2.2 (ruby 3.2.2-p53) ("Speaking of Now")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 171
* Listening on ssl://[::]:9292?cert=pem%2Fall.pem&key=pem%2Fkey.pem&verify_mode=none
Use Ctrl-C to stop
And testing the ssl
% echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
depth=2 O = anchordotdev, CN = development - AnchorCA
verify error:num=19:self signed certificate in certificate chain
verify return:0
DONE
0 s:/O=anchordotdev/CN=anchor.lcl.host
i:/O=anchordotdev/CN=anchor - SubCA
1 s:/O=anchordotdev/CN=anchor - SubCA
i:/O=anchordotdev/CN=development - AnchorCA
2 s:/O=anchordotdev/CN=development - AnchorCA
i:/O=anchordotdev/CN=development - AnchorCA
subject=/O=anchordotdev/CN=anchor.lcl.host
What I would expect -- full cert chain and notification that the self signed cert is there.
ruby puma_ssl_test.rb --test strings
% ruby puma_ssl_test.rb --test strings
Test puma with ssl_bind options:
cert_pem: contents of pem/all.pem
key_pem: contents of pem/key.pem
The cert_pem option contains the certificate and the entire cert chain
Once Puma is running, you can test it with:
echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
Puma starting in single mode...
* Puma version: 6.2.2 (ruby 3.2.2-p53) ("Speaking of Now")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 263
* Listening on ssl://[::]:9292?cert=store%3A0&key=store%3A1&verify_mode=none
Use Ctrl-C to stop
And testing the ssl
% echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
depth=0 O = anchordotdev, CN = anchor.lcl.host
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = anchordotdev, CN = anchor.lcl.host
verify error:num=21:unable to verify the first certificate
verify return:1
DONE
0 s:/O=anchordotdev/CN=anchor.lcl.host
i:/O=anchordotdev/CN=anchor - SubCA
subject=/O=anchordotdev/CN=anchor.lcl.host
issuer=/O=anchordotdev/CN=anchor - SubCA
The cert chain is missing.
Observations
It appears that in order to use the cert_pem:
option and get the same results as using cert:
you must also use the ca:
and verify_mode:
options
ruby puma_ssl_test.rb --test ca
% ruby puma_ssl_test.rb --test ca
Test puma with ssl_bind options:
cert_pem: contents of pem/cert.pem
key_pem: contents of pem/key.pem
ca: pem/ca.pem
verify_mode: peer
The cert_pem option contains only the certificate.
The ca option is the path to the rest of the cert chain
Once Puma is running, you can test it with:
echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
Puma starting in single mode...
* Puma version: 6.2.2 (ruby 3.2.2-p53) ("Speaking of Now")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 379
* Listening on ssl://[::]:9292?cert=store%3A0&key=store%3A1&verify_mode=peer&ca=pem%2Fca.pem
Use Ctrl-C to stop
And testing results in the same result as using cert:
with the path to a file
% echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
depth=2 O = anchordotdev, CN = development - AnchorCA
verify error:num=19:self signed certificate in certificate chain
verify return:0
DONE
0 s:/O=anchordotdev/CN=anchor.lcl.host
i:/O=anchordotdev/CN=anchor - SubCA
1 s:/O=anchordotdev/CN=anchor - SubCA
i:/O=anchordotdev/CN=development - AnchorCA
2 s:/O=anchordotdev/CN=development - AnchorCA
i:/O=anchordotdev/CN=development - AnchorCA
subject=/O=anchordotdev/CN=anchor.lcl.host
issuer=/O=anchordotdev/CN=anchor - SubCA
Questions
- Is this the expected behavior when using
cert_pem:
- Or is it the
cert:
option that is actually out of spec, and they should both be usingca:
- Should there also be a
ca_pem:
option for passing a string based ca option so none of them need to read from disk? - Should we submit a PR that attempts to resolve this and get consistent behavior, or is this something that you are readily familiar enough that would be trivial to resolve for you?
Desktop Environment (please complete the following information)
- OS: macOS Monterey - 12.6.4
- Puma Version Puma version: 6.2.2 (ruby 3.2.2-p53) ("Speaking of Now")
If I recall, there has never been parity between 'file based' and 'string based' functions.
Or, as you've mentioned, there are two 'file based' functions that will process multiple certs, but there are not equivalent string functions. I.think.
So, not sure what to do. The docs could be improved. I can't seriously look at this until this weekend...
I like consistency. There's probably not consistency here because the functionality has been contributed by different parties with different needs over time. Maybe you can dive into the git history/PRs and see what you find?
I know @stanhu of GitLab recently contributed a SSL related thing (#3133), maybe they have some thoughts on this.
SSL_CTX_use_certificate_chain_file
was added with #801, 20-Oct-2015. Found that last night.
At the time, there wasn't an API for using strings. That was added 27-Oct-2021 with #2728.
#3133 added an option to pass a file thru a program/script, decrypting the file. The file doesn't contain useful information until it's decrypted.
I took a look at the implementation of SSL_CTX_use_certificate_chain_file
and it uses the rest of the public api to do its work. In fact the same way that cert_pem=
uses PEM_read_bio_X509
and SSL_CTX_use_certificate
is the first bit of SSL_CTX_use_certificate_chain_file
implementation.
I'll give a go at an initial implementation to make cert=
and cert_pem=
be consistent.
With the patch in the above PR - I now get the same results from the --test strings
test as I do with the --test files
test.
% ruby ssl_app.rb --test strings
Test puma with ssl_bind options:
cert_pem: contents of pem/all.pem
key_pem: contents of pem/key.pem
The cert_pem option contains the certificate and the entire cert chain
Once Puma is running, you can test it with:
echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
Puma starting in single mode...
* Puma version: 6.3.0 (ruby 3.2.2-p53) ("Mugi No Toki Itaru")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 79379
* Listening on ssl://[::]:9292?cert=store%3A0&key=store%3A1&verify_mode=none
Use Ctrl-C to stop
And the ssl client test
% echo | openssl s_client -host anchor.lcl.host -port 9292 -debug -servername anchor.lcl.host | grep 'O=anchordotdev'
depth=2 O = anchordotdev, CN = development - AnchorCA
verify error:num=19:self signed certificate in certificate chain
verify return:0
DONE
0 s:/O=anchordotdev/CN=anchor.lcl.host
i:/O=anchordotdev/CN=anchor - SubCA
1 s:/O=anchordotdev/CN=anchor - SubCA
i:/O=anchordotdev/CN=development - AnchorCA
2 s:/O=anchordotdev/CN=development - AnchorCA
i:/O=anchordotdev/CN=development - AnchorCA
subject=/O=anchordotdev/CN=anchor.lcl.host
issuer=/O=anchordotdev/CN=anchor - SubCA