Using certificates and keys from a list

146 views
Skip to first unread message

Doug Hardie

unread,
Apr 4, 2026, 12:18:02 AM (11 days ago) Apr 4
to openss...@openssl.org
I have an application that needs to dynamically select the certificates and keys. Currently I have it using SSL_use_certificate_file(ssl, file, SSL_FILETYPE_PEM) to read the certificate from a file and SSL_use_PrivateKey_file(ssl, file, SSL_FILETYPE_PEM) to access the keys from a table of file names of the certificates and keys. This works if the key files are world readable. However, the application starts as root, and they switches to user nobody. Hence it is then unable to access the keys if they are properly protected.

The solution is to read the certificates and keys into memory while still running as root. However, it is not obvious which SSL calls should be used. Should I get the sizes of each, allocate memory and then read them into that memory and use a table of those addresses. Then use SSL_use_certificate(SSL *ssl, X509 *x) to load the certificate into the SSL and then SSL_use_PrivateKey(SSL *ssl, EVP_PKEY *pkey) to load the key? It seems like I am missing something.

-- Doug

Doug Hardie

unread,
Apr 4, 2026, 12:26:53 AM (11 days ago) Apr 4
to openss...@openssl.org
> On Apr 3, 2026, at 21:17, Doug Hardie <bc...@lafn.org> wrote:
>
> I have an application that needs to dynamically select the certificates and keys. Currently I have it using SSL_use_certificate_file(ssl, file, SSL_FILETYPE_PEM) to read the certificate from a file and SSL_use_PrivateKey_file(ssl, file, SSL_FILETYPE_PEM) to access the keys from a table of file names of the certificates and keys. This works if the key files are world readable. However, the application starts as root, and they switches to user nobody. Hence it is then unable to access the keys if they are properly protected.
>
> The solution is to read the certificates and keys into memory while still running as root. However, it is not obvious which SSL calls should be used. Should I get the sizes of each, allocate memory and then read them into that memory and use a table of those addresses. Then use SSL_use_certificate(SSL *ssl, X509 *x) to load the certificate into the SSL and then SSL_use_PrivateKey(SSL *ssl, EVP_PKEY *pkey) to load the key? It seems like I am missing something.
>

Should have included that the certificates are chains containing the server certificate and the intermediate certificates.

-- Doug

Viktor Dukhovni

unread,
Apr 4, 2026, 12:46:08 AM (11 days ago) Apr 4
to openss...@openssl.org
On Fri, Apr 03, 2026 at 09:17:41PM -0700, Doug Hardie wrote:

> The solution is to read the certificates and keys into memory while
> still running as root. However, it is not obvious which SSL calls
> should be used. Should I get the sizes of each, allocate memory and
> then read them into that memory and use a table of those addresses.
> Then use SSL_use_certificate(SSL *ssl, X509 *x) to load the
> certificate into the SSL and then SSL_use_PrivateKey(SSL *ssl,
> EVP_PKEY *pkey) to load the key? It seems like I am missing
> something.

The functions you're looking for are SSL_CTX_use_cert_and_key(3) and
SSL_use_cert_and_key(3).

int SSL_CTX_use_cert_and_key(SSL_CTX *ctx, X509 *x, EVP_PKEY *pkey, STACK_OF(X509) *chain, int override);
int SSL_use_cert_and_key(SSL *ssl, X509 *x, EVP_PKEY *pkey, STACK_OF(X509) *chain, int override);

These operate on already decoded keys, certificates and CA certificate
chains. This has been in use in Postfix for some time:

https://github.com/vdukhovni/postfix/blob/250e75ebd980eafa3ed5f25e1d9d6a896b794c2e/postfix/src/tls/tls_certkey.c#L152-L181

In general the Postfix src/tls directory is a good source of clear
examples of correct OpenSSL usage. You can start with tls_server.c
and tls_client.c, and then follow the call chain into supporting
code.

--
Viktor. 🇺🇦 Слава Україні!

Victor Wagner

unread,
Apr 4, 2026, 2:49:57 AM (11 days ago) Apr 4
to openss...@openssl.org
В Fri, 3 Apr 2026 21:17:41 -0700
Doug Hardie <bc...@lafn.org> пишет:

> I have an application that needs to dynamically select the
> certificates and keys. Currently I have it using

> SSL_use_certificate_file(ssl, file, SSL_FILETYPE_PEM) to read the
> certificate from a file and SSL_use_PrivateKey_file(ssl, file,
> SSL_FILETYPE_PEM) to access the keys from a table of file names of
> the certificates and keys. This works if the key files are world
> readable. However, the application starts as root, and they switches
> to user nobody. Hence it is then unable to access the keys if they
> are properly protected.

I recommend you to study source code of Apache Web Server. It has
similar workflow, and its code solves this problem and many others.


>
> The solution is to read the certificates and keys into memory while
> still running as root. However, it is not obvious which SSL calls
> should be used. Should I get the sizes of each, allocate memory and
> then read them into that memory and use a table of those addresses.
> Then use SSL_use_certificate(SSL *ssl, X509 *x) to load the
> certificate into the SSL and then SSL_use_PrivateKey(SSL *ssl,
> EVP_PKEY *pkey) to load the key? It seems like I am missing
> something.
>
> -- Doug
>



--
Victor Wagner <vi...@wagner.pp.ru>

Doug Hardie

unread,
Apr 5, 2026, 4:06:51 PM (9 days ago) Apr 5
to openss...@openssl.org
Should have known that would be the case. I tried chasing down apache, but it is just too convoluted to figure out quickly. I believe I now have the cert and key handled correctly. Still have to work on the chain. However, I did notice in load_pem_object that pkey_type is defined with initial value NID_undef. I can't find anywhere that is changed, but yet it is tested for a variety of different values. I am looking at the source for postfix-3.10.2. Did I miss something?

-- Doug


Viktor Dukhovni

unread,
Apr 5, 2026, 4:32:17 PM (9 days ago) Apr 5
to openss...@openssl.org
On Sun, Apr 05, 2026 at 01:06:29PM -0700, Doug Hardie wrote:

> >> then read them into that memory and use a table of those addresses.
> >> Then use SSL_use_certificate(SSL *ssl, X509 *x) to load the
> >> certificate into the SSL and then SSL_use_PrivateKey(SSL *ssl,
> >> EVP_PKEY *pkey) to load the key? It seems like I am missing
> >> something.

The pkey_type data flow is bit non-obvious, the NID stays NID_undef for
PKCS#8 keys (the norm), which can be decoded generically, otherwise it
is set to a type-specific NID for type-specific PEM encodings:

https://github.com/vdukhovni/postfix/blob/250e75ebd980eafa3ed5f25e1d9d6a896b794c2e/postfix/src/tls/tls_certkey.c#L363-L370

} else if (strcmp(name, PEM_STRING_PKCS8INF) == 0
|| ((pkey_type = EVP_PKEY_RSA) != NID_undef
&& strcmp(name, PEM_STRING_RSA) == 0)
|| ((pkey_type = EVP_PKEY_EC) != NID_undef
&& strcmp(name, PEM_STRING_ECPRIVATEKEY) == 0)
|| ((pkey_type = EVP_PKEY_DSA) != NID_undef
&& strcmp(name, PEM_STRING_DSA) == 0)) {
load_pkey(st, pkey_type, buf, buflen);

Those comparisons with `NID_undef` are assignments, the last one before
a strcmp() matches one wins. Then in load_pkey() the NID is used to
either fall back to PKCS#8 or to pass to d2i_PrivateKey():

https://github.com/vdukhovni/postfix/blob/250e75ebd980eafa3ed5f25e1d9d6a896b794c2e/postfix/src/tls/tls_certkey.c#L258-L266

if (pkey_type != NID_undef) {
pkey = d2i_PrivateKey(pkey_type, 0, &p, buflen);
} else {
p8 = d2i_PKCS8_PRIV_KEY_INFO(NULL, &p, buflen);
if (p8) {
pkey = EVP_PKCS82PKEY(p8);
PKCS8_PRIV_KEY_INFO_free(p8);
}
}

--
Viktor. 🇺🇦 Слава Україні!

Viktor Dukhovni

unread,
Apr 6, 2026, 2:41:52 AM (9 days ago) Apr 6
to openss...@openssl.org
On Mon, Apr 06, 2026 at 06:32:06AM +1000, Viktor Dukhovni wrote:

> The pkey_type data flow is bit non-obvious, the NID stays NID_undef for
> PKCS#8 keys (the norm), which can be decoded generically, otherwise it
> is set to a type-specific NID for type-specific PEM encodings:
>
> https://github.com/vdukhovni/postfix/blob/250e75ebd980eafa3ed5f25e1d9d6a896b794c2e/postfix/src/tls/tls_certkey.c#L363-L370

I should perhaps mention that similar (derived from the original version
in Postfix) code was recently added in OpenSSL, and will be part of the
4.0 release:

https://github.com/openssl/openssl/pull/30089

See:

https://github.com/openssl/openssl/blob/99f50faba8b2ea3cc9bf7174f5efbb7b9dbab6e1/apps/lib/apps.c#L1173-L1277

--
Viktor. 🇺🇦 Слава Україні!

Doug Hardie

unread,
Apr 7, 2026, 2:12:43 AM (8 days ago) Apr 7
to openss...@openssl.org
That is some interesting code. Quite clever. Reminds me of some code a contractor wrote for the USAF (assembly language):

xor a, b bet you can't figure out what this does.
xor b, a
xor a, b

This was way before wikipedia. I had the contractor rewrite the comment to explain what it did.

Thanks to Viktor and Victor, I now have the application working in a test environment. Later this week I'll generate all the needed certificates and put it into production.


-- Doug


Viktor Dukhovni

unread,
Apr 7, 2026, 2:35:34 AM (8 days ago) Apr 7
to openss...@openssl.org
On Mon, Apr 06, 2026 at 11:12:20PM -0700, Doug Hardie wrote:

> That is some interesting code. Quite clever. Reminds me of some code a contractor wrote for the USAF (assembly language):
>
> xor a, b bet you can't figure out what this does.
> xor b, a
> xor a, b

This is clearly a swap of two registers a<->b without touching any other
registers.

> This was way before wikipedia. I had the contractor rewrite the
> comment to explain what it did.

Clearly intended for readers adept in Z_2 module [sic] arithmetic.

a -> (a + b)
b -> b + (a + b) = a
a -> (a + b) + b = a

> Thanks to Viktor and Victor, I now have the application working in a
> test environment. Later this week I'll generate all the needed
> certificates and put it into production.

One limitation may be worth keeping in mind. You finally prodded me
into refactoring and simplifying the Postfix SNI code that uses those
functions, and in particular, in the SNI code path, SSL_use_cert_key(3),
rather than SSL_CTX_use_cert_and_key(3).

Now that OpenSSL 3.5 and later support PQC signature algorithms, on a
whim I tested SNI with ML-DSA-44 as one of the SNI-selected keys.
Sadly, that ran into a null-pointer-exception introduced in 3.2, but
mostly latent until 3.5, unless one loads a provider that adds novel
asymmetric signature algorithms in 3.2–3.4.

There are now open pull requests to fix the bug, in 3.3 and 3.4 (3.2 is
EOL), and in 3.5 through "master". These should be merged shortly. In
the meantime, if you're using 3.5 or 3.6, best to be careful with
SSL_use_cert_and_key(3), and use it only with:

- RSA
- ECDSA
- DSA (obsolete)
- EdDSA (Ed25519 or Ed448)

Once 3.5 and 3.6 are updated, it will become safe to use this also with
ML-DSA (-44, -65 and -87). This should be fixed in 4.0-dev before the
final 4.0 release is shipped.

Thanks for prodding me into taking the code for another spin.

--
Viktor. 🇺🇦 Слава Україні!

Doug Hardie

unread,
Apr 8, 2026, 6:24:35 PM (6 days ago) Apr 8
to openss...@openssl.org
I implemented all that code to load the certificates and keys and it worked just fine. However, once I set the keys to 0600, it no longer worked. The reason is that the startup code that runs as root is quite small. It doesn't know which keys will be required. In fact, those keys might not even exist yet. They can be added at any time. Hence, the app needs to read the keys after the uid has been changed to the unprivileged uid.

The solution turns out to be changing the setuid to seteuid to the unprivileged user, and then when the connection is starting, in the SSL_CTX_set_client_hello_cb routine, use seteuid (0) to go back to root. After the certificate and key are loaded use seteuid to go back to the unprivileged user. This is on FreeBSD. There is a similar, but different implementation for Linux.

-- Doug


Seo Suchan

unread,
Apr 8, 2026, 7:37:36 PM (6 days ago) Apr 8
to openss...@openssl.org
Wouldn't it defeat perpose of dropping privilege in first place if code can call function to raise its privilege back to root?


On 2026년 4월 9일 오전 7시 24분 14초 GMT+09:00, Doug Hardie <bc...@lafn.org> 작성함:

Doug Hardie

unread,
Apr 8, 2026, 8:40:01 PM (6 days ago) Apr 8
to Seo Suchan, openss...@openssl.org
Not really. It only raises privilege for just enough time to read the certificate and key. That code is checked by me to ensure it does nothing unusual.

-- Doug


Viktor Dukhovni

unread,
Apr 8, 2026, 11:31:25 PM (6 days ago) Apr 8
to openss...@openssl.org
In an HTTPS server I implemented, a different approach is used. The
directory in which the keys are stored is owned by "root", with 0700
permissions, but the private key + certificate chain files are world
readable (or group readable if preferred, the server does not care).

The process starts life as "root", opens a directory file descriptor,
and then fully drops privs. When reading certificate files it uses
openat(2) to access the private key and certificate material through
filenames that are relative to the cached directory descriptor.

--
Viktor. 🇺🇦 Слава Україні!

Martin Bonner

unread,
Apr 9, 2026, 2:38:30 AM (6 days ago) Apr 9
to Doug Hardie, Seo Suchan, openss...@openssl.org

Yehbut: If an attacker can get code execution inside your app, you might think they only have the access of the unprivileged user, but actually they just have to do seteuid(0) and the world is their oyster.

 

If an attacker can’t get code execution inside your app, then why bother dropping to the unprivileged user at all?  (One answer to that may be, because the interactive user can tell the app to read and write files, and you want to limit that to the unprivileged user.)

 

Martin Bonner

 

 

From: openss...@openssl.org <openss...@openssl.org> On Behalf Of Doug Hardie
Sent: 09 April 2026 01:40
To: Seo Suchan <tjt...@gmail.com>
Cc: openss...@openssl.org
Subject: [EXTERNAL] Re: Using certificates and keys from a list

 

> On Apr 8, 2026, at 16:37, Seo Suchan <tjtncks@gmail.com> wrote: > > Wouldn't it defeat perpose of dropping privilege in first place if code can call function to raise its privilege back to root? > > > On 2026 4 9

-- 
You received this message because you are subscribed to the Google Groups "openssl-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to openssl-user...@openssl.org.
To view this discussion visit https://urldefense.com/v3/__https://groups.google.com/a/openssl.org/d/msgid/openssl-users/17570E38-919A-47D5-8DA0-53248D5DD490*40sermon-archive.info__;JQ!!FJ-Y8qCqXTj2!cOjW3cavmIqGSZDMJlh9jjTyLtoxc4ayzsyY5HoiOn6iLZp33GZ8kvjRuQfhwYPopJHSNzhFw7gyfh8$.
Any email and files/attachments transmitted with it are intended solely for the use of the individual or entity to whom they are addressed. If this message has been sent to you in error, you must not copy, distribute or disclose of the information it contains. Please notify Entrust immediately and delete the message from your system.

Jochen Bern

unread,
Apr 9, 2026, 6:24:26 AM (6 days ago) Apr 9
to openss...@openssl.org
Am 09.04.26 um 00:24 schrieb Doug Hardie:
> once I set the keys to 0600, it no longer worked. The reason is
> that the startup code that runs as root is quite small. It
> doesn't know which keys will be required. In fact, those keys
> might not even exist yet.

I think that y'all are overdesigning this a bit ... if there's a need
for the process to read secrets from files after startup (read: after
potential malicious influence), then the content of those files is, by
definition, unprotected vis-a-vis the running, possibly subverted,
application. Create a dedicated UID or GID for the process to run as and
let bog-standard permissions do their thing with access attempts from
*else*where (short of local root).

Yes, this means that you might want to look into revocation mechanisms,
shorter lifetimes etc. as tools to get the damage threatened by leaked
secrets reduced again.

Kind regards,
--
Jochen Bern
Systemingenieur
Binect GmbH

Michael Wojcik

unread,
Apr 9, 2026, 9:24:04 AM (5 days ago) Apr 9
to openss...@openssl.org
> From: 'Martin Bonner' via openssl-users <openss...@openssl.org>
> Sent: Thursday, 9 April, 2026 00:38

> If an attacker can’t get code execution inside your app, then why bother dropping
> to the unprivileged user at all?

That threat model is too simplistic. There are potential vulnerabilities which could result in an attacker having limited control over the application's behavior, insufficient to revert the effective UID but capable of taking other actions. Your example of file I/O is an obvious one, but there are plenty of others.

Changing the EUID is defense in depth.

That said, it's probably not the architecture I'd use; I prefer stronger separation between privileged and non-privileged functions. But I don't know enough about the application and its use cases to define one more to my liking.

--
Michael Wojcik
================================
Rocket Software, Inc. and subsidiaries ■ 77 Fourth Avenue, Waltham MA 02451 ■ Main Office Toll Free Number: +1 855.577.4323
Contact Customer Support: https://my.rocketsoftware.com/RocketCommunity/RCEmailSupport
Unsubscribe from Marketing Messages/Manage Your Subscription Preferences - http://www.rocketsoftware.com/manage-your-email-preferences
Privacy Policy - http://www.rocketsoftware.com/company/legal/privacy-policy
================================

This communication and any attachments may contain confidential information of Rocket Software, Inc. All unauthorized use, disclosure or distribution is prohibited. If you are not the intended recipient, please notify Rocket Software immediately and destroy all copies of this communication. Thank you.

Doug Hardie

unread,
Apr 9, 2026, 11:48:29 AM (5 days ago) Apr 9
to Jochen Bern, openss...@openssl.org

-- Doug

> On Apr 9, 2026, at 03:24, Jochen Bern <Joche...@binect.de> wrote:
>
> Am 09.04.26 um 00:24 schrieb Doug Hardie:
>> once I set the keys to 0600, it no longer worked. The reason is
>> that the startup code that runs as root is quite small. It
>> doesn't know which keys will be required. In fact, those keys
>> might not even exist yet.
>
> I think that y'all are overdesigning this a bit ...


Definitely. This app needs to access key files during the SSL Client Hello processing. That is the first packet sent by the client after the TCP initiation. It includes the SNI field which is needed to find the proper key. All of this takes place prior to any client request. The only thing that takes place before the client hello call back is the initialization of the SSL parameters. The privilege is given back up at the end of the call back. The one valid concern is that retaining the UID of root throughout the remainder of the app might somehow reset the euid back to root. Hence, I am changing the last part of the call back to setuid to the unprivileged user. I believe that will prevent any follow on code from changing it.

Note, the app has been in daily use since the mid 90's. I am the only developer of it, and I most likely have the only instance of it. There is no user written code involved. The protection is there so that any errors in the code will not affect any other processes.

> if there's a need for the process to read secrets from files after startup (read: after potential malicious influence), then the content of those files is, by definition, unprotected vis-a-vis the running, possibly subverted, application. Create a dedicated UID or GID for the process to run as and let bog-standard permissions do their thing with access attempts from *else*where (short of local root).
>
> Yes, this means that you might want to look into revocation mechanisms, shorter lifetimes etc. as tools to get the damage threatened by leaked secrets reduced again.
>

Root life time is just long enough to decode the SNI field and read the two files into SSL.

-- Doug


Jochen Bern

unread,
Apr 9, 2026, 12:19:10 PM (5 days ago) Apr 9
to openss...@openssl.org, Doug Hardie
Am 09.04.26 um 17:48 schrieb Doug Hardie:
> This app needs to access key files during the SSL Client Hello processing.
> [...] The only thing that takes place before the client hello call back
> is the initialization of the SSL parameters. [...] The one valid concern
> is that retaining the UID of root throughout the remainder of the app
> might somehow reset the euid back to root. Hence, I am changing the
> last part of the call back to setuid to the unprivileged user. I
> believe that will prevent any follow on code from changing it.

Does that mean that your server-side application (instance/process)
*terminates* after processing *one* TCP connection (might preclude TLS
session reuse)? In that case, I'd agree that fully relinquishing the
elevated rights ASAP (just with an adapted definition of "AP") would be
even better.

Doug Hardie

unread,
Apr 9, 2026, 7:19:49 PM (5 days ago) Apr 9
to Jochen Bern, openss...@openssl.org
That is correct. Once the TCP connection closes, the app terminates. It only handles on connection. There is just too much instance information stored in the app to be able to easily cleanse it to handle another connection. It is just easier to limit it to one connection. The connection rate is quite low, so the additional overhead of forking for each connection is not significant.

-- Doug


Reply all
Reply to author
Forward
0 new messages