Skip to content

Commit 3874a27

Browse files
committed
Support password callbacks for ECH keys.
1 parent 6ebce14 commit 3874a27

5 files changed

Lines changed: 94 additions & 38 deletions

File tree

‎Doc/library/ssl.rst‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,13 +1749,17 @@ to speed up repeated connections from the same clients.
17491749

17501750
.. versionadded:: 3.4
17511751

1752-
.. method:: SSLContext.load_ech_keys(keyfile, *, for_retry=True)
1752+
.. method:: SSLContext.load_ech_keys(keyfile, password=None, *, for_retry=True)
17531753

17541754
Load a server's Encrypted Client Hello (ECH) private key and configuration.
1755-
The *keyfile* must be the path to a PEM file containing an unencrypted
1756-
private key followed by its matching ECHConfigList. Load the server's TLS
1755+
The *keyfile* must be the path to a PEM file containing a private key
1756+
followed by its matching ECHConfigList. Load the server's TLS
17571757
certificate and private key separately with :meth:`load_cert_chain`.
17581758

1759+
The *password* argument is used to decrypt encrypted private keys. It accepts
1760+
a string, bytes, bytearray, or a callable returning one of those types, as in
1761+
:meth:`load_cert_chain`. Passwords are never requested interactively.
1762+
17591763
Each call adds keys and configurations to those already loaded into the
17601764
context. If *for_retry* is true, the new configurations are included in
17611765
ECH retry configurations sent to clients.

‎Lib/test/certdata/ech-encrypted.pem‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Lib/test/test_ssl.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,23 @@ def test_load_ech_keys(self):
23172317
self.assertEqual(client.get_ech_status(),
23182318
ssl.ECHStatus.ECH_STATUS_SUCCESS)
23192319

2320+
@requires_ech_x25519
2321+
def test_load_ech_keys_password(self):
2322+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2323+
# ech.pem's key encrypted with:
2324+
# openssl pkey -in ech.pem -aes-256-cbc -passout pass:somepass
2325+
filename = data_file('ech-encrypted.pem')
2326+
for password in (KEY_PASSWORD, lambda: KEY_PASSWORD):
2327+
self.assertIsNone(ctx.load_ech_keys(filename, password=password))
2328+
2329+
def password_error():
2330+
raise ValueError('password callback failed')
2331+
2332+
with self.assertRaisesRegex(ValueError, 'password callback failed'):
2333+
ctx.load_ech_keys(filename, password=password_error)
2334+
self.assertIsNone(ctx.load_ech_keys(data_file('ech.pem'),
2335+
password=password_error))
2336+
23202337
@requires_ech_x25519
23212338
def test_ech_handshake(self):
23222339
key, config = self.read_ech_key_and_config()

‎Modules/_ssl.c‎

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5442,20 +5442,14 @@ _ssl__SSLContext_set_ech_config_impl(PySSLContext *self,
54425442
}
54435443

54445444
static int
5445-
ech_password_callback(char *buf, int size, int rwflag, void *userdata)
5446-
{
5447-
/* ECH key files must be unencrypted. Never prompt for a password. */
5448-
return -1;
5449-
}
5450-
5451-
static int
5452-
load_ech_keys(SSL_CTX *ctx, BIO *bio, int for_retry)
5445+
load_ech_keys(SSL_CTX *ctx, BIO *bio, int for_retry,
5446+
_PySSLPasswordInfo *pw_info)
54535447
{
54545448
int result = 0;
54555449
int old_keys, new_keys;
54565450
OSSL_ECHSTORE *store = NULL;
54575451
EVP_PKEY *key = PEM_read_bio_PrivateKey(bio, NULL,
5458-
ech_password_callback, NULL);
5452+
_password_callback, pw_info);
54595453
if (key == NULL) {
54605454
goto end;
54615455
}
@@ -5488,60 +5482,80 @@ load_ech_keys(SSL_CTX *ctx, BIO *bio, int for_retry)
54885482
}
54895483

54905484
/*[clinic input]
5491-
@critical_section
54925485
_ssl._SSLContext.load_ech_keys
54935486
54945487
keyfile: object
5488+
password: object = None
54955489
*
54965490
for_retry: bool = True
54975491
54985492
Load server ECH keys and configurations from a PEM file.
54995493
5500-
The file must contain an unencrypted private key followed by its
5501-
ECHConfigList.
5494+
The file must contain a private key followed by its ECHConfigList.
5495+
The password argument may be a string, bytes, bytearray, or a
5496+
callable returning one of those types. It is only needed for
5497+
encrypted keys.
55025498
If for_retry is true, advertise the configurations in ECH retry
55035499
responses.
55045500
[clinic start generated code]*/
55055501

55065502
static PyObject *
55075503
_ssl__SSLContext_load_ech_keys_impl(PySSLContext *self, PyObject *keyfile,
5508-
int for_retry)
5509-
/*[clinic end generated code: output=8de2cc58ce2f8077 input=3e6617d346227acd]*/
5504+
PyObject *password, int for_retry)
5505+
/*[clinic end generated code: output=73b9b4832b89f40d input=016a625a1505de4a]*/
55105506
{
55115507
PyObject *keyfile_bytes;
5508+
PyObject *ret = NULL;
5509+
_PySSLPasswordInfo pw_info = { NULL, NULL, NULL, 0, 0 };
55125510
if (!PyUnicode_FSConverter(keyfile, &keyfile_bytes)) {
55135511
return NULL;
55145512
}
5513+
if (password != Py_None) {
5514+
if (PyCallable_Check(password)) {
5515+
pw_info.callable = password;
5516+
} else if (!_pwinfo_set(&pw_info, password,
5517+
"password should be a string or callable")) {
5518+
goto done;
5519+
}
5520+
}
55155521
const char *filename = PyBytes_AS_STRING(keyfile_bytes);
55165522
int result = 0;
55175523
int open_failed = 0;
55185524

55195525
ERR_clear_error();
5520-
PySSL_BEGIN_ALLOW_THREADS(self)
5526+
PyMutex_Lock(&self->tstate_mutex);
5527+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
55215528
errno = 0;
55225529
BIO *bio = BIO_new_file(filename, "rb");
55235530
if (bio == NULL) {
55245531
open_failed = 1;
55255532
}
55265533
else {
5527-
result = load_ech_keys(self->ctx, bio, for_retry);
5534+
result = load_ech_keys(self->ctx, bio, for_retry, &pw_info);
55285535
BIO_free(bio);
55295536
}
5530-
PySSL_END_ALLOW_THREADS(self)
5537+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
5538+
PyMutex_Unlock(&self->tstate_mutex);
55315539

55325540
if (result != 1) {
5533-
if (open_failed && errno != 0) {
5541+
if (pw_info.error) {
5542+
/* The password callback has already set the exception. */
5543+
ERR_clear_error();
5544+
}
5545+
else if (open_failed && errno != 0) {
55345546
PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, keyfile);
55355547
ERR_clear_error();
55365548
}
55375549
else {
55385550
_setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
55395551
}
5540-
Py_DECREF(keyfile_bytes);
5541-
return NULL;
5552+
goto done;
55425553
}
5554+
ret = Py_NewRef(Py_None);
5555+
done:
5556+
PyMem_Free(pw_info.password);
55435557
Py_DECREF(keyfile_bytes);
5544-
Py_RETURN_NONE;
5558+
return ret;
55455559
}
55465560
#endif /* HAVE_OPENSSL_ECH */
55475561

‎Modules/clinic/_ssl.c.h‎

Lines changed: 25 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)