From 755b1c1fc6e3590f72b22b9b8d0b81771a905aa7 Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Wed, 29 Apr 2026 16:44:30 +0200 Subject: [PATCH 1/4] crypto: add get_jwk_from_pkcs12() To allow signing web tokens with the help of libjose the new function get_jwk_from_pkcs12() can generate a JSON Web Key (JWK) from a given PKCS#12 file. --- Makefile.am | 6 +- src/tests/cmocka/test_cert_utils.c | 48 +++ src/tests/test_CA/Makefile.am | 8 +- src/tests/test_ECC_CA/Makefile.am | 8 +- src/util/cert.h | 5 + src/util/cert/libcrypto/cert.c | 578 +++++++++++++++++++++++++++++ 6 files changed, 647 insertions(+), 6 deletions(-) diff --git a/Makefile.am b/Makefile.am index 346a56a8037..d9278861a39 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1035,11 +1035,13 @@ libsss_cert_la_SOURCES = \ libsss_cert_la_CFLAGS = \ $(AM_CFLAGS) \ $(SSS_CERT_CFLAGS) \ + $(JOSE_CFLAGS) \ $(NULL) libsss_cert_la_LIBADD = \ $(SSS_CERT_LIBS) \ $(TALLOC_LIBS) \ $(TEVENT_LIBS) \ + $(JOSE_LIBS) \ libsss_crypt.la \ libsss_debug.la \ $(NULL) @@ -2011,8 +2013,8 @@ libsss_certmap_la_SOURCES += \ src/util/crypto/libcrypto/crypto_base64.c \ src/util/cert/libcrypto/cert.c \ $(NULL) -libsss_certmap_la_CFLAGS += $(CRYPTO_CFLAGS) -libsss_certmap_la_LIBADD += $(CRYPTO_LIBS) +libsss_certmap_la_CFLAGS += $(CRYPTO_CFLAGS) $(JOSE_CFLAGS) +libsss_certmap_la_LIBADD += $(CRYPTO_LIBS) $(JOSE_LIBS) dist_noinst_DATA += src/lib/certmap/sss_certmap.exports dist_noinst_HEADERS += src/lib/certmap/sss_certmap_int.h diff --git a/src/tests/cmocka/test_cert_utils.c b/src/tests/cmocka/test_cert_utils.c index dff23365438..2bb5a820b88 100644 --- a/src/tests/cmocka/test_cert_utils.c +++ b/src/tests/cmocka/test_cert_utils.c @@ -42,6 +42,8 @@ #include "tests/test_CA/SSSD_test_cert_x509_0007.h" #include "tests/test_ECC_CA/SSSD_test_ECC_cert_pubsshkey_0001.h" #include "tests/test_ECC_CA/SSSD_test_ECC_cert_x509_0001.h" +#include "tests/test_CA/SSSD_test_cert_pkcs12_0001.h" +#include "tests/test_ECC_CA/SSSD_test_ECC_cert_pkcs12_0001.h" #else #define SSSD_TEST_CERT_0001 "" #define SSSD_TEST_CERT_SSH_KEY_0001 "" @@ -51,6 +53,8 @@ #define SSSD_TEST_CERT_SSH_KEY_0007 "" #define SSSD_TEST_ECC_CERT_0001 "" #define SSSD_TEST_ECC_CERT_SSH_KEY_0001 "" +#define SSSD_TEST_PKCS12_0001 "" +#define SSSD_TEST_ECC_PKCS12_0001 "" #endif /* When run under valgrind with --trace-children=yes we have to increase the @@ -825,6 +829,48 @@ void test_cert_to_ssh_2keys_with_certmap_2_send(void **state) talloc_free(ev); } +void test_get_jwk_from_pkcs12(void **state) +{ + int ret; + char *jwk = NULL; + uint8_t *pkcs12; + size_t pkcs12_size; + const char rsa_prefix[] = "{\"keys\":[{\"kty\":\"RSA\","; + const char ecc_prefix[] = "{\"keys\":[{\"kty\":\"EC\","; + + struct test_state *ts = talloc_get_type_abort(*state, struct test_state); + assert_non_null(ts); + ts->done = false; + + ret = get_jwk_from_pkcs12(ts, NULL, 0, NULL, &jwk); + assert_int_equal(ret, EINVAL); + + pkcs12 = sss_base64_decode(ts, SSSD_TEST_PKCS12_0001, &pkcs12_size); + assert_non_null(pkcs12); + + ret = get_jwk_from_pkcs12(ts, pkcs12, pkcs12_size, "123456", &jwk); + assert_int_equal(ret, EOK); + assert_non_null(jwk); + assert_true(strlen(jwk) > strlen(rsa_prefix)); + assert_true(strncmp(jwk, rsa_prefix, strlen(rsa_prefix)) == 0); + + talloc_free(pkcs12); + talloc_free(jwk); + jwk = NULL; + + pkcs12 = sss_base64_decode(ts, SSSD_TEST_ECC_PKCS12_0001, &pkcs12_size); + assert_non_null(pkcs12); + + ret = get_jwk_from_pkcs12(ts, pkcs12, pkcs12_size, "123456", &jwk); + assert_int_equal(ret, EOK); + assert_non_null(jwk); + assert_true(strlen(jwk) > strlen(ecc_prefix)); + assert_true(strncmp(jwk, ecc_prefix, strlen(ecc_prefix)) == 0); + + talloc_free(pkcs12); + talloc_free(jwk); +} + int main(int argc, const char *argv[]) { poptContext pc; @@ -864,6 +910,8 @@ int main(int argc, const char *argv[]) setup, teardown), cmocka_unit_test_setup_teardown(test_cert_to_ssh_2keys_with_certmap_2_send, setup, teardown), + cmocka_unit_test_setup_teardown(test_get_jwk_from_pkcs12, + setup, teardown), #endif }; diff --git a/src/tests/test_CA/Makefile.am b/src/tests/test_CA/Makefile.am index 5a8f855a346..86657884e08 100644 --- a/src/tests/test_CA/Makefile.am +++ b/src/tests/test_CA/Makefile.am @@ -36,6 +36,7 @@ certs_h = $(addprefix SSSD_test_cert_x509_,$(addsuffix .h,$(ids))) pubkeys = $(addprefix SSSD_test_cert_pubsshkey_,$(addsuffix .pub,$(ids))) pubkeys_h = $(addprefix SSSD_test_cert_pubsshkey_,$(addsuffix .h,$(ids))) pkcs12 = $(addprefix SSSD_test_cert_pkcs12_,$(addsuffix .pem,$(ids))) +pkcs12_h = $(addprefix SSSD_test_cert_pkcs12_,$(addsuffix .h,$(ids))) extra = softhsm2_none softhsm2_one softhsm2_two softhsm2_2tokens softhsm2_ocsp softhsm2_2certs_same_id softhsm2_pss_one softhsm2_revoked SSSD_test_cert_x509_0001.der SSSD_test_cert_x509_0007.der @@ -53,7 +54,7 @@ endif # If openssl is run in parallel there might be conflicts with the serial .NOTPARALLEL: -ca_all: clean serial SSSD_test_CA.pem $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) $(extra) +ca_all: clean serial SSSD_test_CA.pem $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) $(pkcs12_h) $(extra) $(pwdfile): @echo "123456" > $@ @@ -121,6 +122,9 @@ SSSD_test_cert_x509_%.h: SSSD_test_cert_x509_%.pem SSSD_test_cert_pubsshkey_%.h: SSSD_test_cert_pubsshkey_%.pub @echo "#define SSSD_TEST_CERT_SSH_KEY_$* \""$(shell cut -d' ' -f2 $<)"\"" > $@ +SSSD_test_cert_pkcs12_%.h: SSSD_test_cert_pkcs12_%.pem + @echo "#define SSSD_TEST_PKCS12_$* \""$(shell cat $< | base64 -w 0)"\"" > $@ + SSSD_test_CA_expired_crl.pem: $(FAKETIME) -f '-7d' $(OPENSSL) ca -gencrl -out $@ -keyfile $(openssl_ca_key) -config ${openssl_ca_config} -crlhours 1 @@ -259,7 +263,7 @@ CLEANFILES = \ serial serial.old \ SSSD_test_CA.pem $(pwdfile) SSSD_test_CA_expired_crl.pem \ SSSD_test_CA_crl.pem \ - $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) \ + $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) $(pkcs12_h) \ softhsm2_*.conf \ SSSD_test_*.der \ $(NULL) diff --git a/src/tests/test_ECC_CA/Makefile.am b/src/tests/test_ECC_CA/Makefile.am index 0bc2f78e94a..15a5468ef6f 100644 --- a/src/tests/test_ECC_CA/Makefile.am +++ b/src/tests/test_ECC_CA/Makefile.am @@ -15,13 +15,14 @@ certs_h = $(addprefix SSSD_test_ECC_cert_x509_,$(addsuffix .h,$(ids))) pubkeys = $(addprefix SSSD_test_ECC_cert_pubsshkey_,$(addsuffix .pub,$(ids))) pubkeys_h = $(addprefix SSSD_test_ECC_cert_pubsshkey_,$(addsuffix .h,$(ids))) pkcs12 = $(addprefix SSSD_test_ECC_cert_pkcs12_,$(addsuffix .pem,$(ids))) +pkcs12_h = $(addprefix SSSD_test_ECC_cert_pkcs12_,$(addsuffix .h,$(ids))) extra = softhsm2_ecc_one SSSD_test_ECC_crl.pem # If openssl is run in parallel there might be conflicts with the serial .NOTPARALLEL: -ca_all: clean serial SSSD_test_ECC_crl.pem $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) $(extra) +ca_all: clean serial SSSD_test_ECC_crl.pem $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) $(pkcs12_h) $(extra) $(pwdfile): @echo "123456" > $@ @@ -51,6 +52,9 @@ SSSD_test_ECC_cert_x509_%.h: SSSD_test_ECC_cert_x509_%.pem SSSD_test_ECC_cert_pubsshkey_%.h: SSSD_test_ECC_cert_pubsshkey_%.pub @echo "#define SSSD_TEST_ECC_CERT_SSH_KEY_$* \""$(shell cut -d' ' -f2 $<)"\"" > $@ +SSSD_test_ECC_cert_pkcs12_%.h: SSSD_test_ECC_cert_pkcs12_%.pem + @echo "#define SSSD_TEST_ECC_PKCS12_$* \""$(shell cat $< | base64 -w 0)"\"" > $@ + SSSD_test_ECC_crl.pem: $(openssl_ecc_ca_key) SSSD_test_ECC_CA.pem $(OPENSSL) ca -gencrl -out $@ -keyfile $(openssl_ecc_ca_key) -config $(openssl_ecc_ca_config) -crldays 99999 @@ -71,7 +75,7 @@ CLEANFILES = \ index.txt.attr.old index.txt.old \ serial serial.old \ SSSD_test_ECC_CA.pem SSSD_test_ECC_crl.pem $(pwdfile) \ - $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) \ + $(certs) $(certs_h) $(pubkeys) $(pubkeys_h) $(pkcs12) $(pkcs12_h) \ softhsm2_*.conf \ $(NULL) diff --git a/src/util/cert.h b/src/util/cert.h index 0c95865b692..4e58d8504b5 100644 --- a/src/util/cert.h +++ b/src/util/cert.h @@ -47,4 +47,9 @@ errno_t get_ssh_key_from_cert(TALLOC_CTX *mem_ctx, errno_t get_ssh_key_from_derb64(TALLOC_CTX *mem_ctx, const char *derb64, uint8_t **key_blob, size_t *key_size); + +errno_t get_jwk_from_pkcs12(TALLOC_CTX *mem_ctx, + const uint8_t *der_blob, size_t der_size, + const char *password, + char **_jwk); #endif /* __CERT_H__ */ diff --git a/src/util/cert/libcrypto/cert.c b/src/util/cert/libcrypto/cert.c index c9732b9a7fd..adaa0f2343a 100644 --- a/src/util/cert/libcrypto/cert.c +++ b/src/util/cert/libcrypto/cert.c @@ -18,14 +18,17 @@ */ #include +#include #include #include #if OPENSSL_VERSION_NUMBER >= 0x30000000L #include #endif +#include "jose/b64.h" #include "util/util.h" #include "util/sss_endian.h" +#include "util/crypto/sss_crypto.h" errno_t sss_cert_der_to_pem(TALLOC_CTX *mem_ctx, const uint8_t *der_blob, size_t der_size, char **pem, size_t *pem_size) @@ -179,6 +182,459 @@ errno_t sss_cert_pem_to_der(TALLOC_CTX *mem_ctx, const char *pem, #define IDENTIFIER_NISTP384 "nistp384" #define IDENTIFIER_NISTP521 "nistp521" + +static char *sss_jose_b64_enc_buf(TALLOC_CTX *mem_ctx, + unsigned char *in, size_t in_len) +{ + char *out = NULL; + size_t out_len; + + out_len = jose_b64_enc_buf(in, in_len, NULL, 0); + if (out_len == 0 || out_len == SIZE_MAX) { + DEBUG(SSSDBG_OP_FAILURE, "jose_b64_enc_buf() failed.\n"); + return NULL; + } + + out = talloc_zero_size(mem_ctx, out_len + 1); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_size() failed.\n"); + return NULL; + } + + out_len = jose_b64_enc_buf(in, in_len, out, out_len); + if (out_len == 0 || out_len == SIZE_MAX) { + talloc_free(out); + DEBUG(SSSDBG_OP_FAILURE, "jose_b64_enc_buf() failed.\n"); + return NULL; + } + + return out; +} + +static int sss_bn_to_base64(TALLOC_CTX *mem_ctx, const BIGNUM *in, char **_out) +{ + int len; + int ret; + unsigned char *buf; + + len = BN_num_bytes(in); + if (len == 0) { + DEBUG(SSSDBG_OP_FAILURE, "Given BIGNUM has len 0.\n"); + return EINVAL; + } + + buf = talloc_size(mem_ctx, len); + if (buf == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_size() failed.\n"); + return ENOMEM; + } + + ret = BN_bn2bin(in, buf); + if (ret != len) { + DEBUG(SSSDBG_OP_FAILURE, "Return of BN_bn2bin and BN_num_bytes differ.\n"); + ret = EINVAL; + goto done; + } + + *_out = sss_jose_b64_enc_buf(mem_ctx, buf, len); + if (*_out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "sss_jose_b64_enc_buf() failed.\n"); + ret = ENOMEM; + goto done; + } + + ret = EOK; +done: + talloc_free(buf); + + return ret; +} + +static int sss_ec_get_x_y_d(BN_CTX *bn_ctx, const EVP_PKEY *cert_pub_key, + EC_GROUP **_ec_group, + BIGNUM **_x, BIGNUM **_y, BIGNUM **_d) +{ + int ret; + BIGNUM *x = NULL; + BIGNUM *y = NULL; + BIGNUM *d = NULL; + static char curve_name[4096]; + EC_GROUP *ec_group = NULL; + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_EC_PUB_X, &x); + if (ret != 1) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC x coordinate.\n"); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_EC_PUB_Y, &y); + if (ret != 1) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC y coordinate.\n"); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_PRIV_KEY, &d); + if (ret != 1) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC y coordinate.\n"); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_utf8_string_param(cert_pub_key, + OSSL_PKEY_PARAM_GROUP_NAME, + curve_name, sizeof(curve_name), NULL); + if (ret != 1) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC group name.\n"); + ret = EINVAL; + goto done; + } + + ec_group = EC_GROUP_new_by_curve_name(OBJ_sn2nid(curve_name)); + if (ec_group == NULL) { + ret = EINVAL; + goto done; + } + + *_x = x; + *_y = y; + *_d = d; + *_ec_group = ec_group; + + ret = EOK; + +done: + if (ret != EOK) { + BN_free(x); + BN_free(y); + BN_free(d); + EC_GROUP_free(ec_group); + } + + return ret; +} + +#define CRV_P256 "P-256" +#define CRV_P384 "P-384" +#define CRV_P521 "P-521" + +static errno_t ec_priv_key_jwk(TALLOC_CTX *mem_ctx, EVP_PKEY *cert_priv_key, + const char *cert_hash, char **_jwk) +{ + int ret; + BIGNUM *x = NULL; + BIGNUM *y = NULL; + BIGNUM *d = NULL; + EC_GROUP *ec_group = NULL; + const char *jwk_crv; + char *out = NULL; + char *x_str = NULL; + char *y_str = NULL; + char *d_str = NULL; + BN_CTX *bn_ctx = NULL; + + bn_ctx = BN_CTX_new(); + if (bn_ctx == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "BN_CTX_new failed.\n"); + ret = ENOMEM; + goto done; + } + + ret = sss_ec_get_x_y_d(bn_ctx, cert_priv_key, &ec_group, &x, &y, &d); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC parameters.\n"); + goto done; + } + + switch(EC_GROUP_get_curve_name(ec_group)) { + case NID_X9_62_prime256v1: + jwk_crv = CRV_P256; + break; + case NID_secp384r1: + jwk_crv = CRV_P384; + break; + case NID_secp521r1: + jwk_crv = CRV_P521; + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "Unsupported curve [%s]\n", + OBJ_nid2sn(EC_GROUP_get_curve_name(ec_group))); + ret = EINVAL; + goto done; + } + + ret = sss_bn_to_base64(mem_ctx, x, &x_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, y, &y_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, d, &d_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + if (x_str == NULL || y_str == NULL || d_str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to convert BIGNUM to base64.\n"); + ret = ENOMEM; + goto done; + } + + out = talloc_asprintf(mem_ctx, + "\"kty\":\"EC\"," + "\"crv\":\"%s\"," + "\"x\":\"%s\"," + "\"y\":\"%s\"," + "\"d\":\"%s\"," + "\"x5t#S256\":\"%s\"", jwk_crv, x_str, y_str, d_str, + cert_hash); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JSON snippet.\n"); + ret = ENOMEM; + goto done; + } + + *_jwk = out; + + ret = EOK; +done: + talloc_free(x_str); + talloc_free(y_str); + talloc_free(d_str); + BN_free(x); + BN_free(y); + BN_free(d); + EC_GROUP_free(ec_group); + BN_CTX_free(bn_ctx); + return ret; +} + +static int sss_rsa_get_priv_comps(const EVP_PKEY *cert_pub_key, + BIGNUM **_n, BIGNUM **_e, + BIGNUM **_d, + BIGNUM **_p, BIGNUM **_q, + BIGNUM **_dp, BIGNUM **_dq, + BIGNUM **_qi) +{ + int ret; + BIGNUM *n = NULL; + BIGNUM *e = NULL; + BIGNUM *d = NULL; + BIGNUM *p = NULL; + BIGNUM *q = NULL; + BIGNUM *dp = NULL; + BIGNUM *dq = NULL; + BIGNUM *qi = NULL; + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_N, &n); + if (ret != 1) { + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_E, &e); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_D, &d); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_FACTOR1, &p); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_FACTOR2, &q); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_EXPONENT1, &dp); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_EXPONENT2, &dq); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, &qi); + if (ret != 1) { + BN_clear_free(n); + ret = EINVAL; + goto done; + } + + *_e = e; + *_n = n; + *_d = d; + *_p = p; + *_q = q; + *_dp = dp; + *_dq = dq; + *_qi = qi; + + ret = EOK; + +done: + if (ret != EOK) { + BN_free(n); + BN_free(e); + BN_free(d); + BN_free(p); + BN_free(q); + BN_free(dp); + BN_free(dq); + BN_free(qi); + } + + return ret; +} + +errno_t rsa_priv_key_jwk(TALLOC_CTX *mem_ctx, EVP_PKEY *cert_priv_key, + const char *cert_hash, char **_jwk) +{ + BIGNUM *n = NULL; + BIGNUM *e = NULL; + BIGNUM *d = NULL; + BIGNUM *p = NULL; + BIGNUM *q = NULL; + BIGNUM *dp = NULL; + BIGNUM *dq = NULL; + BIGNUM *qi = NULL; + int ret; + char *out = NULL; + char *n_str = NULL; + char *e_str = NULL; + char *d_str = NULL; + char *p_str = NULL; + char *q_str = NULL; + char *dp_str = NULL; + char *dq_str = NULL; + char *qi_str = NULL; + + ret = sss_rsa_get_priv_comps(cert_priv_key, &n, &e, &d, &p, &q, + &dp, &dq, &qi); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve RSA parameters.\n"); + goto done; + } + + ret = sss_bn_to_base64(mem_ctx, n, &n_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, e, &e_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, d, &d_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, p, &p_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, q, &q_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, dp, &dp_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, dq, &dq_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + ret = sss_bn_to_base64(mem_ctx, qi, &qi_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "sss_bn_to_base64() failed.\n"); + goto done; + } + if (n_str == NULL || e_str == NULL || d_str == NULL || p_str == NULL + || q_str == NULL || dp_str == NULL || dq_str == NULL + || qi_str == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to convert BIGNUM to base64.\n"); + ret = ENOMEM; + goto done; + } + + out = talloc_asprintf(mem_ctx, + "\"kty\":\"RSA\"," + "\"n\":\"%s\"," + "\"e\":\"%s\"," + "\"d\":\"%s\"," + "\"p\":\"%s\"," + "\"q\":\"%s\"," + "\"dp\":\"%s\"," + "\"dq\":\"%s\"," + "\"qi\":\"%s\"," + "\"alg\":\"%s\"," + "\"x5t#S256\":\"%s\"", n_str, e_str, d_str, p_str, + q_str, dp_str, dq_str, qi_str, + "RS256", cert_hash); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JSON snippet.\n"); + ret = ENOMEM; + goto done; + } + + *_jwk = out; + + ret = EOK; +done: + + talloc_free(n_str); + talloc_free(e_str); + talloc_free(d_str); + talloc_free(p_str); + talloc_free(q_str); + talloc_free(dp_str); + talloc_free(dq_str); + talloc_free(qi_str); + + BN_free(n); + BN_free(e); + BN_free(d); + BN_free(p); + BN_free(q); + BN_free(dp); + BN_free(dq); + BN_free(qi); + + return ret; +} + #if OPENSSL_VERSION_NUMBER < 0x30000000L static int sss_ec_get_key(BN_CTX *bn_ctx, EVP_PKEY *cert_pub_key, #else @@ -570,3 +1026,125 @@ errno_t get_ssh_key_from_cert(TALLOC_CTX *mem_ctx, return ret; } + +static char *get_cert_sha256_hash(TALLOC_CTX *mem_ctx, X509 *cert) +{ + EVP_MD *md = NULL; + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + char *out; + int ret; + + md = EVP_MD_fetch(NULL, "sha256", NULL); + if (md == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize hashing.\n"); + return NULL; + } + + ret = X509_digest(cert, md, md_value, &md_len); + EVP_MD_free(md); + if (ret != 1) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to calculate hash.\n"); + return NULL; + } + + out = sss_jose_b64_enc_buf(mem_ctx, md_value, md_len); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to base64-encode hash value.\n"); + return NULL; + } + + return out; +} + +errno_t get_jwk_from_pkcs12(TALLOC_CTX *mem_ctx, + const uint8_t *der_blob, size_t der_size, + const char *password, + char **_jwk) +{ + int ret; + const unsigned char *d; + PKCS12 *pkcs12 = NULL; + EVP_PKEY *pkey = NULL; + X509 *cert = NULL; + char *jwk_base = NULL; + char *jwk = NULL; + char *cert_hash = NULL; + + if (der_blob == NULL || der_size == 0) { + return EINVAL; + } + + d = (const unsigned char *) der_blob; + + pkcs12 = d2i_PKCS12(NULL, &d, (int) der_size); + if (pkcs12 == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "d2i_PKCS12 failed.\n"); + return EINVAL; + } + + ret = PKCS12_parse(pkcs12, password, &pkey, &cert, NULL); + if (ret != 1) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to extract private key.\n"); + ret = EIO; + goto done; + } + + cert_hash = get_cert_sha256_hash(mem_ctx, cert); + if (cert_hash == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to get SHA-256 hash of certificate.\n"); + ret = EIO; + goto done; + } + + switch (EVP_PKEY_base_id(pkey)) { + case EVP_PKEY_RSA: + ret = rsa_priv_key_jwk(mem_ctx, pkey, cert_hash, &jwk_base); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "rsa_priv_key_jwk failed.\n"); + goto done; + } + break; + case EVP_PKEY_EC: + ret = ec_priv_key_jwk(mem_ctx, pkey, cert_hash, &jwk_base); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "ec_priv_key_jwk failed.\n"); + goto done; + } + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "Expected RSA or EC public key, found unsupported [%d].\n", + EVP_PKEY_base_id(pkey)); + ret = EINVAL; + goto done; + } + + if (jwk_base == NULL || *jwk_base == '\0') { + DEBUG(SSSDBG_OP_FAILURE, "Missing JWK key data.\n"); + ret = EINVAL; + goto done; + } + + /* optional "use" or "kid" items can be added here */ + jwk = talloc_asprintf(mem_ctx, "{\"keys\":[{%s}]}", jwk_base); + talloc_free(jwk_base); + if (jwk == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JWK.\n"); + ret = ENOMEM; + goto done; + } + + *_jwk = jwk; + + ret = EOK; + +done: + EVP_PKEY_free(pkey); + PKCS12_free(pkcs12); + X509_free(cert); + talloc_free(cert_hash); + + return ret; +} From 5c22f4e8b336579344d55203fe77404638f52a98 Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Wed, 15 Apr 2026 16:56:31 +0200 Subject: [PATCH 2/4] oidc_child: add pkcs12-client-creds option With the new option pkcs12-client-creds a PKCS#12 file with certificate and private key can be specified for client authentication. The client credential will be used as a password to unlock the key in the PKCS#12 file. The PKCS#12 file is used in a way to make mutual TLS (mTLS) work with an IdP. --- src/oidc_child/oidc_child.c | 17 ++++++++++--- src/oidc_child/oidc_child_curl.c | 43 +++++++++++++++++++++++++++++--- src/oidc_child/oidc_child_id.c | 4 ++- src/oidc_child/oidc_child_util.h | 5 +++- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/oidc_child/oidc_child.c b/src/oidc_child/oidc_child.c index 31f675e7cad..f5dd6f3be25 100644 --- a/src/oidc_child/oidc_child.c +++ b/src/oidc_child/oidc_child.c @@ -283,7 +283,9 @@ static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, const char *device_auth_endpoint, const char *token_endpoint, const char *userinfo_endpoint, - const char *jwks_uri, const char *scope) + const char *jwks_uri, const char *scope, + const char *pkcs12_client_creds, + const char *key_passwd) { struct devicecode_ctx *dc_ctx = NULL; int ret; @@ -295,7 +297,8 @@ static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, goto done; } - dc_ctx->rest_ctx = get_rest_ctx(dc_ctx, libcurl_debug, ca_db); + dc_ctx->rest_ctx = get_rest_ctx(dc_ctx, libcurl_debug, ca_db, + pkcs12_client_creds, key_passwd); if (dc_ctx->rest_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get curl context.\n"); ret = ENOMEM; @@ -353,6 +356,7 @@ struct cli_opts { char *search_str; char *idp_type; bool return_tokens; + char *pkcs12_client_creds; }; static void free_cli_opts_members(struct cli_opts *opts) @@ -372,6 +376,7 @@ static void free_cli_opts_members(struct cli_opts *opts) free(opts->user_identifier_attr); free(opts->search_str); free(opts->idp_type); + free(opts->pkcs12_client_creds); } static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) @@ -425,6 +430,8 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) NULL}, {"object-id", 0, POPT_ARG_STRING, &tmp_obj_id, 0, _("Object ID of user or group"), NULL}, + {"pkcs12-client-creds", 0, POPT_ARG_STRING, &opts->pkcs12_client_creds, 0, + _("Client certificate and key in PKCS#12 format"), NULL}, {"ca-db", 0, POPT_ARG_STRING, &opts->ca_db, 0, _("Path to PEM file with CA certificates"), NULL}, {"return-tokens", 0, POPT_ARG_NONE, NULL, 'r', @@ -655,6 +662,7 @@ int main(int argc, const char *argv[]) opts.search_str, opts.search_str_type, opts.libcurl_debug, opts.ca_db, opts.client_id, opts.client_secret, + opts.pkcs12_client_creds, opts.token_endpoint, opts.scope, &out); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Id lookup failed.\n"); @@ -675,7 +683,10 @@ int main(int argc, const char *argv[]) dc_ctx = get_dc_ctx(main_ctx, opts.libcurl_debug, opts.ca_db, opts.issuer_url, opts.device_auth_endpoint, opts.token_endpoint, - opts.userinfo_endpoint, opts.jwks_uri, opts.scope); + opts.userinfo_endpoint, opts.jwks_uri, opts.scope, + opts.pkcs12_client_creds, + opts.pkcs12_client_creds == NULL ? NULL + : opts.client_secret); if (dc_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize main context.\n"); goto done; diff --git a/src/oidc_child/oidc_child_curl.c b/src/oidc_child/oidc_child_curl.c index 55305652a31..282e6e49cca 100644 --- a/src/oidc_child/oidc_child_curl.c +++ b/src/oidc_child/oidc_child_curl.c @@ -30,6 +30,8 @@ struct rest_ctx { bool libcurl_debug; const char *ca_db; + const char *pkcs12_client_creds; + const char *key_passwd; char *http_data; CURL *curl_ctx; }; @@ -57,7 +59,9 @@ static CURL *init_curl(void) static int rest_ctx_destructor(void *p); struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, - const char *ca_db) + const char *ca_db, + const char *pkcs12_client_creds, + const char *key_passwd) { struct rest_ctx *rest_ctx; @@ -77,6 +81,17 @@ struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, } } + if (pkcs12_client_creds != NULL) { + rest_ctx->pkcs12_client_creds = talloc_strdup(rest_ctx, + pkcs12_client_creds); + if (rest_ctx->pkcs12_client_creds == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to copy PKCS#12 path.\n"); + goto fail; + } + + rest_ctx->key_passwd = key_passwd; + } + rest_ctx->curl_ctx = init_curl(); if (rest_ctx->curl_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize curl.\n"); @@ -346,6 +361,27 @@ static errno_t set_http_opts(CURL *curl_ctx, struct rest_ctx *rest_ctx, ret = EIO; goto done; } + } else if (rest_ctx->pkcs12_client_creds != NULL) { + res = curl_easy_setopt(curl_ctx, CURLOPT_SSLCERT, + rest_ctx->pkcs12_client_creds); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set path the PKCS#12 file.\n"); + ret = EIO; + goto done; + } + res = curl_easy_setopt(curl_ctx, CURLOPT_SSLCERTTYPE, "P12"); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set cert type.\n"); + ret = EIO; + goto done; + } + res = curl_easy_setopt(curl_ctx, CURLOPT_KEYPASSWD, + rest_ctx->key_passwd); + if (res != CURLE_OK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set key password.\n"); + ret = EIO; + goto done; + } } ret = EOK; @@ -456,7 +492,8 @@ errno_t do_http_request(struct rest_ctx *rest_ctx, const char *uri, goto done; } - ret = set_http_opts(curl_ctx, rest_ctx, uri, post_data, token, headers); + ret = set_http_opts(curl_ctx, rest_ctx, uri, post_data, + token, headers); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Failed to set http options.\n"); goto done; @@ -685,7 +722,7 @@ errno_t get_devicecode(struct devicecode_ctx *dc_ctx, goto done; } - if (client_secret != NULL) { + if (client_secret != NULL && dc_ctx->rest_ctx->pkcs12_client_creds == NULL) { post_data = append_to_post_data(post_data, "client_secret", client_secret); if (post_data == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to add client_secret to POST data.\n"); diff --git a/src/oidc_child/oidc_child_id.c b/src/oidc_child/oidc_child_id.c index d4bb04a436f..c1654932178 100644 --- a/src/oidc_child/oidc_child_id.c +++ b/src/oidc_child/oidc_child_id.c @@ -470,6 +470,7 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, char *input, enum search_str_type input_type, bool libcurl_debug, const char *ca_db, const char *client_id, const char *client_secret, + const char *pkcs12_client_creds, const char *token_endpoint, const char *scope, char **out) { errno_t ret; @@ -488,7 +489,8 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, return EINVAL; } - rest_ctx = get_rest_ctx(mem_ctx, libcurl_debug, ca_db); + rest_ctx = get_rest_ctx(mem_ctx, libcurl_debug, ca_db, + pkcs12_client_creds, client_secret); if (rest_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get REST context.\n"); return ENOMEM; diff --git a/src/oidc_child/oidc_child_util.h b/src/oidc_child/oidc_child_util.h index 8d251654410..50230002ea8 100644 --- a/src/oidc_child/oidc_child_util.h +++ b/src/oidc_child/oidc_child_util.h @@ -89,7 +89,9 @@ struct name_and_type_identifier { /* oidc_child_curl.c */ struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, - const char *ca_db); + const char *ca_db, + const char *pkcs12_client_creds, + const char *key_passwd); const char *get_http_data(struct rest_ctx *rest_ctx); @@ -188,6 +190,7 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, char *input, enum search_str_type input_type, bool libcurl_debug, const char *ca_db, const char *client_id, const char *client_secret, + const char *pkcs12_client_creds, const char *token_endpoint, const char *scope, char **out); #endif /* __OIDC_CHILD_UTIL_H__ */ From 4a3d6e5c7b9a03b72fa9072c3ccd370fb7349d6a Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Tue, 12 May 2026 19:31:37 +0200 Subject: [PATCH 3/4] oidc_child: add JWT authentication With the new option --client-auth-method oidc_child can select between authentication with a client secret, mutual-TLS/mTLS (RFC-8705) and JWT client assertion (RFC-7523). The latter require a PKCS#12 file with the client credentials (certificate and private key) and the password to unlock the private key must be provided with the --client-secret or --client-secret-stdin option. --- Makefile.am | 5 + src/oidc_child/oidc_child.c | 84 ++++++++++++- src/oidc_child/oidc_child_curl.c | 127 +++++++++++++++---- src/oidc_child/oidc_child_id.c | 4 +- src/oidc_child/oidc_child_json.c | 205 +++++++++++++++++++++++++++++++ src/oidc_child/oidc_child_util.h | 17 +++ src/util/cert/libcrypto/cert.c | 12 +- 7 files changed, 423 insertions(+), 31 deletions(-) diff --git a/Makefile.am b/Makefile.am index d9278861a39..d8ba5180c91 100644 --- a/Makefile.am +++ b/Makefile.am @@ -4949,6 +4949,7 @@ oidc_child_SOURCES = \ src/util/strtonum.c \ src/util/sss_chain_id.c \ src/util/sss_prctl.c \ + src/util/cert/libcrypto/cert.c \ $(NULL) oidc_child_CFLAGS = \ $(AM_CFLAGS) \ @@ -4956,6 +4957,8 @@ oidc_child_CFLAGS = \ $(JANSSON_CFLAGS) \ $(JOSE_CFLAGS) \ $(CURL_CFLAGS) \ + $(UUID_CFLAGS) \ + $(CRYPTO_CFLAGS) \ $(NULL) oidc_child_LDADD = \ libsss_debug.la \ @@ -4964,6 +4967,8 @@ oidc_child_LDADD = \ $(JANSSON_LIBS) \ $(JOSE_LIBS) \ $(CURL_LIBS) \ + $(UUID_LIBS) \ + $(CRYPTO_LIBS) \ $(NULL) endif diff --git a/src/oidc_child/oidc_child.c b/src/oidc_child/oidc_child.c index f5dd6f3be25..a38ef9515ea 100644 --- a/src/oidc_child/oidc_child.c +++ b/src/oidc_child/oidc_child.c @@ -285,6 +285,7 @@ static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, const char *userinfo_endpoint, const char *jwks_uri, const char *scope, const char *pkcs12_client_creds, + enum client_auth_method client_auth_method, const char *key_passwd) { struct devicecode_ctx *dc_ctx = NULL; @@ -298,7 +299,8 @@ static struct devicecode_ctx *get_dc_ctx(TALLOC_CTX *mem_ctx, } dc_ctx->rest_ctx = get_rest_ctx(dc_ctx, libcurl_debug, ca_db, - pkcs12_client_creds, key_passwd); + pkcs12_client_creds, client_auth_method, + key_passwd); if (dc_ctx->rest_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get curl context.\n"); ret = ENOMEM; @@ -357,6 +359,7 @@ struct cli_opts { char *idp_type; bool return_tokens; char *pkcs12_client_creds; + enum client_auth_method client_auth_method; }; static void free_cli_opts_members(struct cli_opts *opts) @@ -379,6 +382,71 @@ static void free_cli_opts_members(struct cli_opts *opts) free(opts->pkcs12_client_creds); } +static bool has_creds_client_secret(struct cli_opts *opts) +{ + return (opts->client_secret != NULL || opts->client_secret_stdin); +} + +static bool has_creds_pkcs12(struct cli_opts *opts) +{ + return (has_creds_client_secret(opts) && opts->pkcs12_client_creds != NULL); +} + +static bool check_client_auth(const char *tmp_cam, struct cli_opts *opts) +{ + if (tmp_cam != NULL) { + if (strcasecmp(tmp_cam, "none") == 0) { + opts->client_auth_method = CAM_NONE; + } else if (strcasecmp(tmp_cam, "secret") == 0) { + opts->client_auth_method = CAM_SECRET; + } else if (strcasecmp(tmp_cam, "mtls") == 0) { + opts->client_auth_method = CAM_MTLS; + } else if (strcasecmp(tmp_cam, "jwt") == 0) { + opts->client_auth_method = CAM_JWT; + } else { + fprintf(stderr, + "\nUnsupported client authentication method [%s].\n", + tmp_cam); + return false; + } + } else { + opts->client_auth_method = CAM_SECRET; + } + + switch (opts->client_auth_method) { + case CAM_SECRET: + if (!has_creds_client_secret(opts)) { + fprintf(stderr, "\nMissing client secret.\n"); + return false; + } + if (has_creds_pkcs12(opts)) { + fprintf(stderr, "\nPath to PKCS12 file is not needed for " + "authentication with client secret.\n"); + return false; + } + break; + case CAM_MTLS: + case CAM_JWT: + if (!has_creds_pkcs12(opts)) { + fprintf(stderr, "\nPath to PKCS12 file and password/secret " + "are needed for client authentication.\n"); + return false; + } + break; + case CAM_NONE: + if (has_creds_client_secret(opts)) { + fprintf(stderr, "\nClient secrets are not expected.\n"); + return false; + } + break; + default: + fprintf(stderr, "\nUnsupported client authentication method.\n"); + return false; + } + + return true; +} + static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) { poptContext pc; @@ -387,6 +455,7 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) bool print_usage = true; char *tmp_name = NULL; char *tmp_obj_id = NULL; + char *tmp_cam = NULL; struct poptOption long_options[] = { SSSD_BASIC_CHILD_OPTS @@ -421,9 +490,9 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) _("Supported scope of the IdP to get userinfo"), NULL}, {"client-id", 0, POPT_ARG_STRING, &opts->client_id, 0, _("Client ID"), NULL}, {"client-secret", 0, POPT_ARG_STRING, &opts->client_secret, 0, - _("Client secret (if needed)"), NULL}, + _("Client secret/PKCS#12 password (if needed)"), NULL}, {"client-secret-stdin", 0, POPT_ARG_NONE, NULL, 's', - _("Read client secret from standard input"), NULL}, + _("Read client secret/PKCS#12 password from standard input"), NULL}, {"idp-type", 0, POPT_ARG_STRING, &opts->idp_type, 0, _("Type of the IdP (entra_id, keycloak etc)"), NULL}, {"name", 0, POPT_ARG_STRING, &tmp_name, 0, _("Name of user or group"), @@ -432,6 +501,8 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) _("Object ID of user or group"), NULL}, {"pkcs12-client-creds", 0, POPT_ARG_STRING, &opts->pkcs12_client_creds, 0, _("Client certificate and key in PKCS#12 format"), NULL}, + {"client-auth-method", 0, POPT_ARG_STRING, &tmp_cam, 0, + _("Authentication method against IdP [secret, mtls, jwt]"), NULL}, {"ca-db", 0, POPT_ARG_STRING, &opts->ca_db, 0, _("Path to PEM file with CA certificates"), NULL}, {"return-tokens", 0, POPT_ARG_NONE, NULL, 'r', @@ -498,6 +569,10 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) goto done; } + if (!check_client_auth(tmp_cam, opts)) { + goto done; + } + if (tmp_name != NULL) { opts->search_str = tmp_name; opts->search_str_type = TYPE_NAME; @@ -521,6 +596,7 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) ret = EOK; done: + free(tmp_cam); if (print_usage) { poptPrintUsage(pc, stderr, 0); poptFreeContext(pc); @@ -663,6 +739,7 @@ int main(int argc, const char *argv[]) opts.libcurl_debug, opts.ca_db, opts.client_id, opts.client_secret, opts.pkcs12_client_creds, + opts.client_auth_method, opts.token_endpoint, opts.scope, &out); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Id lookup failed.\n"); @@ -685,6 +762,7 @@ int main(int argc, const char *argv[]) opts.device_auth_endpoint, opts.token_endpoint, opts.userinfo_endpoint, opts.jwks_uri, opts.scope, opts.pkcs12_client_creds, + opts.client_auth_method, opts.pkcs12_client_creds == NULL ? NULL : opts.client_secret); if (dc_ctx == NULL) { diff --git a/src/oidc_child/oidc_child_curl.c b/src/oidc_child/oidc_child_curl.c index 282e6e49cca..41487448e95 100644 --- a/src/oidc_child/oidc_child_curl.c +++ b/src/oidc_child/oidc_child_curl.c @@ -31,6 +31,7 @@ struct rest_ctx { bool libcurl_debug; const char *ca_db; const char *pkcs12_client_creds; + enum client_auth_method client_auth_method; const char *key_passwd; char *http_data; CURL *curl_ctx; @@ -61,6 +62,7 @@ static int rest_ctx_destructor(void *p); struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, const char *ca_db, const char *pkcs12_client_creds, + enum client_auth_method client_auth_method, const char *key_passwd) { struct rest_ctx *rest_ctx; @@ -91,6 +93,7 @@ struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, rest_ctx->key_passwd = key_passwd; } + rest_ctx->client_auth_method = client_auth_method; rest_ctx->curl_ctx = init_curl(); if (rest_ctx->curl_ctx == NULL) { @@ -126,6 +129,16 @@ errno_t set_http_data(struct rest_ctx *rest_ctx, const char *str) return EOK; } +const char *rest_ctx_get_pkcs12_client_creds(struct rest_ctx *rest_ctx) +{ + return (const char *) rest_ctx->pkcs12_client_creds; +} + +const char *rest_ctx_get_key_passwd(struct rest_ctx *rest_ctx) +{ + return (const char *) rest_ctx->key_passwd; +} + char *url_encode_string(struct rest_ctx *rest_ctx, const char *inp) { char *tmp; @@ -361,7 +374,7 @@ static errno_t set_http_opts(CURL *curl_ctx, struct rest_ctx *rest_ctx, ret = EIO; goto done; } - } else if (rest_ctx->pkcs12_client_creds != NULL) { + } else if (rest_ctx->client_auth_method == CAM_MTLS) { res = curl_easy_setopt(curl_ctx, CURLOPT_SSLCERT, rest_ctx->pkcs12_client_creds); if (res != CURLE_OK) { @@ -527,6 +540,65 @@ errno_t do_http_request(struct rest_ctx *rest_ctx, const char *uri, return ret; } +static char *append_jwt_to_postdata(char *str, struct rest_ctx *rest_ctx, + const char *token_endpoint, + const char *client_id) +{ + char *out = NULL; + char *jwt = NULL; + + out = append_to_post_data(str, "client_assertion_type", + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client_assertion_type to POST data.\n"); + return out; + } + + jwt = get_jwt(rest_ctx, token_endpoint, client_id); + if (jwt == NULL) { + talloc_free(out); + DEBUG(SSSDBG_OP_FAILURE, "Failed to get JWT.\n"); + return NULL; + } + + out = append_to_post_data(out, "client_assertion", jwt); + talloc_free(jwt); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client_assertion to POST data.\n"); + return NULL; + } + + return out; +} + +static char *append_to_creds_to_post_data(char *str, + struct rest_ctx *rest_ctx, + const char *token_endpoint, + const char *client_id, + const char *client_secret) +{ + char *out = str; + + if (rest_ctx->client_auth_method == CAM_SECRET) { + out = append_to_post_data(out, "client_secret", client_secret); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to add client_secret to POST data.\n"); + return NULL; + } + } else if (rest_ctx->client_auth_method == CAM_JWT) { + out = append_jwt_to_postdata(out, rest_ctx, token_endpoint, client_id); + if (out == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add JWT client assertion to POST data.\n"); + return NULL; + } + } + + return out; +} + #define AZURE_EXPECT_CODE "The request body must contain the following parameter: 'code'." errno_t get_token(TALLOC_CTX *mem_ctx, @@ -564,13 +636,14 @@ errno_t get_token(TALLOC_CTX *mem_ctx, goto done; } - if (client_secret != NULL) { - post_data = append_to_post_data(post_data, "client_secret", client_secret); - if (post_data == NULL) { - DEBUG(SSSDBG_OP_FAILURE, "Failed to add client_secret to POST data.\n"); - ret = ENOMEM; - goto done; - } + post_data = append_to_creds_to_post_data(post_data, dc_ctx->rest_ctx, + dc_ctx->token_endpoint, + client_id, client_secret); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client credentials to POST data.\n"); + ret = ENOMEM; + goto done; } /* Remember the offset of the device code for the azure fallback later. */ @@ -722,15 +795,17 @@ errno_t get_devicecode(struct devicecode_ctx *dc_ctx, goto done; } - if (client_secret != NULL && dc_ctx->rest_ctx->pkcs12_client_creds == NULL) { - post_data = append_to_post_data(post_data, "client_secret", client_secret); - if (post_data == NULL) { - DEBUG(SSSDBG_OP_FAILURE, "Failed to add client_secret to POST data.\n"); - ret = ENOMEM; - goto done; - } + post_data = append_to_creds_to_post_data(post_data, dc_ctx->rest_ctx, + dc_ctx->token_endpoint, + client_id, client_secret); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client credentials to POST data.\n"); + ret = ENOMEM; + goto done; } + clean_http_data(dc_ctx->rest_ctx); ret = do_http_request(dc_ctx->rest_ctx, dc_ctx->device_authorization_endpoint, @@ -806,7 +881,9 @@ errno_t client_credentials_grant(struct rest_ctx *rest_ctx, goto done; } - post_data = append_to_post_data(post_data, "client_secret", client_secret); + post_data = append_to_creds_to_post_data(post_data, rest_ctx, + token_endpoint, client_id, + client_secret); if (post_data == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to add client_secret to POST data.\n"); ret = ENOMEM; @@ -825,7 +902,8 @@ errno_t client_credentials_grant(struct rest_ctx *rest_ctx, clean_http_data(rest_ctx); ret = do_http_request(rest_ctx, token_endpoint, post_data, NULL); if (ret != EOK) { - DEBUG(SSSDBG_OP_FAILURE, "Failed to send device code request.\n"); + DEBUG(SSSDBG_OP_FAILURE, + "Failed to send client credential grant request.\n"); } done: @@ -864,13 +942,14 @@ errno_t refresh_token(TALLOC_CTX *mem_ctx, goto done; } - if (client_secret != NULL) { - post_data = append_to_post_data(post_data, "client_secret", client_secret); - if (post_data == NULL) { - DEBUG(SSSDBG_OP_FAILURE, "Failed to add client_secret to POST data.\n"); - ret = ENOMEM; - goto done; - } + post_data = append_to_creds_to_post_data(post_data, dc_ctx->rest_ctx, + dc_ctx->token_endpoint, + client_id, client_secret); + if (post_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to add client credentials to POST data.\n"); + ret = ENOMEM; + goto done; } post_data = append_to_post_data(post_data, "scope", scope); diff --git a/src/oidc_child/oidc_child_id.c b/src/oidc_child/oidc_child_id.c index c1654932178..76ca1d19a6f 100644 --- a/src/oidc_child/oidc_child_id.c +++ b/src/oidc_child/oidc_child_id.c @@ -471,6 +471,7 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, bool libcurl_debug, const char *ca_db, const char *client_id, const char *client_secret, const char *pkcs12_client_creds, + enum client_auth_method client_auth_method, const char *token_endpoint, const char *scope, char **out) { errno_t ret; @@ -490,7 +491,8 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, } rest_ctx = get_rest_ctx(mem_ctx, libcurl_debug, ca_db, - pkcs12_client_creds, client_secret); + pkcs12_client_creds, client_auth_method, + client_secret); if (rest_ctx == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get REST context.\n"); return ENOMEM; diff --git a/src/oidc_child/oidc_child_json.c b/src/oidc_child/oidc_child_json.c index 89b984691ee..9fb274369c0 100644 --- a/src/oidc_child/oidc_child_json.c +++ b/src/oidc_child/oidc_child_json.c @@ -27,8 +27,11 @@ #include #include #include +#include +#include #include "util/strtonum.h" +#include "util/cert.h" #include "oidc_child/oidc_child_util.h" static char *get_json_string(TALLOC_CTX *mem_ctx, const json_t *root, @@ -53,6 +56,28 @@ static char *get_json_string(TALLOC_CTX *mem_ctx, const json_t *root, return str; } +static const char *get_json_const_string_from_keys(const json_t *root, + const char *attr) +{ + json_t *keys; + json_t *tmp; + + keys = json_object_get(root, "keys"); + if (!json_is_array(keys)) { + DEBUG(SSSDBG_OP_FAILURE, "Missing keys array.\n"); + return NULL; + } + + tmp = json_object_get(json_array_get(keys, 0), attr); + if (!json_is_string(tmp)) { + DEBUG(SSSDBG_OP_FAILURE, + "Result does not contain the '%s' string.\n", attr); + return NULL; + } + + return json_string_value(tmp); +} + static int get_json_integer(const json_t *root, const char *attr, bool fallback_to_string) { @@ -1094,3 +1119,183 @@ json_t *token_data_to_json(struct devicecode_ctx *dc_ctx) json_decref(obj); return NULL; } + +json_t *get_jwk(struct rest_ctx *rest_ctx) +{ + int ret; + int fd = -1; + struct stat st; + char *jwk_str; + json_t *jwk = NULL; + uint8_t *buf = NULL; + json_error_t json_error; + + fd = open(rest_ctx_get_pkcs12_client_creds(rest_ctx), O_RDONLY); + if (fd == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "open() failed [%d][%s]\n", ret, strerror(ret)); + goto done; + } + ret = fstat(fd, &st); + if (ret != 0) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "stat() failed [%d][%s]\n", ret, strerror(ret)); + goto done; + } + buf = talloc_size(rest_ctx, st.st_size); + if (buf == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to allocate buffer for PKCS#12.\n"); + goto done; + } + if (sss_atomic_read_s(fd, buf, st.st_size) != st.st_size) { + DEBUG(SSSDBG_CRIT_FAILURE, "sss_atomic_read_s() failed\n"); + goto done; + } + + ret = get_jwk_from_pkcs12(rest_ctx, buf, st.st_size, + rest_ctx_get_key_passwd(rest_ctx), &jwk_str); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get JWK from PKCS#12.\n"); + goto done; + } + + jwk = json_loads(jwk_str, 0, &json_error); + if (jwk == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "Failed to parse JWK string on line [%d]: [%s].\n", + json_error.line, json_error.text); + ret = EINVAL; + goto done; + } + + ret = EOK; + +done: + talloc_free(jwk_str); + talloc_free(buf); + if (fd != -1) { + close(fd); + } + + return jwk; +} + +static json_t *get_jwt_payload(const char *token_endpoint, + const char *client_id) +{ + time_t iat = time(NULL); + time_t nbf = iat - 5; + time_t exp = iat + 60; + const char *aud = token_endpoint; + const char *iss = client_id; + const char *sub = client_id; + uuid_t uuid; + char jti[UUID_STR_LEN]; + json_t *payload = NULL; + json_t *payload_b64 = NULL; + json_t *json = NULL; + + uuid_generate(uuid); + uuid_unparse(uuid, jti); + + payload = json_pack("{s:s, s:s, s:s, s:s, s:I, s:I, s:I}", + "aud", aud, + "iss", iss, + "sub", sub, + "jti", jti, + "iat", (json_int_t) iat, + "nbf", (json_int_t) nbf, + "exp", (json_int_t) exp); + if (payload == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JSON JWT payload.\n"); + return NULL; + } + + payload_b64 = jose_b64_enc_dump(payload); + json_decref(payload); + if (payload_b64 == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to encode payload.\n"); + return NULL; + } + + json = json_pack("{s:o}", "payload", payload_b64); + if (json == NULL) { + json_decref(payload_b64); + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JSON payload.\n"); + return NULL; + } + + return json; +} + +char *get_jwt(struct rest_ctx *rest_ctx, const char *token_endpoint, + const char *client_id) +{ + const char *payload_str = NULL; + const char *protected_str = NULL; + const char *signature_str = NULL; + json_t *payload = NULL; + json_t *signature_template = NULL; + json_t *jwk = NULL; + char *jwt = NULL; + const char *alg = NULL; + const char *cert_hash = NULL; + + payload = get_jwt_payload(token_endpoint, client_id); + if (payload == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get JWT payload.\n"); + goto done; + } + + jwk = get_jwk(rest_ctx); + if (jwk == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to get JWK.\n"); + goto done; + } + + alg = get_json_const_string_from_keys(jwk, "alg"); + cert_hash = get_json_const_string_from_keys(jwk, "x5t#S256"); + if (alg == NULL || cert_hash == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "JWK is missing required attributes.\n"); + goto done; + } + + signature_template = json_pack("{s:{s:s, s:s, s:s}}", + "protected", + "alg", alg, + "typ", "JWT", + "x5t#S256", cert_hash); + if (signature_template == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate signature template.\n"); + goto done; + } + + if (!jose_jws_sig(NULL, payload, signature_template, jwk)) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to sign JWT.\n"); + goto done; + } + + if (json_unpack(payload, "{s:s, s:s, s:s}", + "payload", &payload_str, + "protected", &protected_str, + "signature", &signature_str) != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to extract JWT components.\n"); + goto done; + } + + jwt = talloc_asprintf(rest_ctx, "%s.%s.%s", + protected_str, payload_str, signature_str); + if (jwt == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JWT string.\n"); + goto done; + } + +done: + json_decref(signature_template); + json_decref(jwk); + json_decref(payload); + + return jwt; +} diff --git a/src/oidc_child/oidc_child_util.h b/src/oidc_child/oidc_child_util.h index 50230002ea8..5d87e8d6981 100644 --- a/src/oidc_child/oidc_child_util.h +++ b/src/oidc_child/oidc_child_util.h @@ -45,6 +45,14 @@ enum search_str_type { TYPE_OBJECT_ID = 2 }; +enum client_auth_method { + CAM_NONE = 0, + CAM_SECRET, + CAM_MTLS, /* RFC-8705 */ + CAM_JWT, /* RFC-7523 */ + CAM_SENTINEL +}; + struct rest_ctx; struct token_data { @@ -91,6 +99,7 @@ struct name_and_type_identifier { struct rest_ctx *get_rest_ctx(TALLOC_CTX *mem_ctx, bool libcurl_debug, const char *ca_db, const char *pkcs12_client_creds, + enum client_auth_method client_auth_method, const char *key_passwd); const char *get_http_data(struct rest_ctx *rest_ctx); @@ -134,6 +143,10 @@ errno_t do_http_request(struct rest_ctx *rest_ctx, const char *uri, errno_t do_http_request_json_data(struct rest_ctx *rest_ctx, const char *uri, const char *post_data, const char *token); +const char *rest_ctx_get_pkcs12_client_creds(struct rest_ctx *rest_ctx); + +const char *rest_ctx_get_key_passwd(struct rest_ctx *rest_ctx); + /* oidc_child_json.c */ errno_t parse_openid_configuration(struct devicecode_ctx *dc_ctx); @@ -184,6 +197,9 @@ errno_t add_posix_to_json_string_array(TALLOC_CTX *mem_ctx, json_t *token_data_to_json(struct devicecode_ctx *dc_ctx); +char *get_jwt(struct rest_ctx *rest_ctx, const char *token_endpoint, + const char *client_id); + /* oidc_child_id.c */ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, char *idp_type, @@ -191,6 +207,7 @@ errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd, bool libcurl_debug, const char *ca_db, const char *client_id, const char *client_secret, const char *pkcs12_client_creds, + enum client_auth_method client_auth_method, const char *token_endpoint, const char *scope, char **out); #endif /* __OIDC_CHILD_UTIL_H__ */ diff --git a/src/util/cert/libcrypto/cert.c b/src/util/cert/libcrypto/cert.c index adaa0f2343a..d0d547d8d1b 100644 --- a/src/util/cert/libcrypto/cert.c +++ b/src/util/cert/libcrypto/cert.c @@ -258,7 +258,7 @@ static int sss_ec_get_x_y_d(BN_CTX *bn_ctx, const EVP_PKEY *cert_pub_key, BIGNUM *x = NULL; BIGNUM *y = NULL; BIGNUM *d = NULL; - static char curve_name[4096]; + char curve_name[4096]; EC_GROUP *ec_group = NULL; ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_EC_PUB_X, &x); @@ -277,7 +277,7 @@ static int sss_ec_get_x_y_d(BN_CTX *bn_ctx, const EVP_PKEY *cert_pub_key, ret = EVP_PKEY_get_bn_param(cert_pub_key, OSSL_PKEY_PARAM_PRIV_KEY, &d); if (ret != 1) { - DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC y coordinate.\n"); + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve EC private key.\n"); ret = EINVAL; goto done; } @@ -333,6 +333,7 @@ static errno_t ec_priv_key_jwk(TALLOC_CTX *mem_ctx, EVP_PKEY *cert_priv_key, char *y_str = NULL; char *d_str = NULL; BN_CTX *bn_ctx = NULL; + const char *alg; bn_ctx = BN_CTX_new(); if (bn_ctx == NULL) { @@ -350,12 +351,15 @@ static errno_t ec_priv_key_jwk(TALLOC_CTX *mem_ctx, EVP_PKEY *cert_priv_key, switch(EC_GROUP_get_curve_name(ec_group)) { case NID_X9_62_prime256v1: jwk_crv = CRV_P256; + alg = "ES256"; break; case NID_secp384r1: jwk_crv = CRV_P384; + alg = "ES384"; break; case NID_secp521r1: jwk_crv = CRV_P521; + alg = "ES512"; break; default: DEBUG(SSSDBG_CRIT_FAILURE, "Unsupported curve [%s]\n", @@ -388,10 +392,12 @@ static errno_t ec_priv_key_jwk(TALLOC_CTX *mem_ctx, EVP_PKEY *cert_priv_key, out = talloc_asprintf(mem_ctx, "\"kty\":\"EC\"," "\"crv\":\"%s\"," + "\"alg\":\"%s\"," "\"x\":\"%s\"," "\"y\":\"%s\"," "\"d\":\"%s\"," - "\"x5t#S256\":\"%s\"", jwk_crv, x_str, y_str, d_str, + "\"x5t#S256\":\"%s\"", jwk_crv, alg, + x_str, y_str, d_str, cert_hash); if (out == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to generate JSON snippet.\n"); From 31242a696e4f76b321f65a1a494fa3100a181adc Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Fri, 15 May 2026 10:08:01 +0200 Subject: [PATCH 4/4] test: add tests for oidc_child 'get-device-code' This new test call oidc_child with the '--get-device-code' option with different client authentication methods. --- src/tests/system/tests/test_oidc_child.py | 195 ++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/src/tests/system/tests/test_oidc_child.py b/src/tests/system/tests/test_oidc_child.py index ecccc7c21ab..db94cfdd00e 100644 --- a/src/tests/system/tests/test_oidc_child.py +++ b/src/tests/system/tests/test_oidc_child.py @@ -21,6 +21,40 @@ "--client-id=myclient --client-secret=ClientSecret123 --scope='profile'" ) +args_get_device_code = ( + "--libcurl-debug -d 9 --logger=stderr " + "--get-device-code --issuer-url=https://master.keycloak.test:8443/auth/realms/master" +) + + +# https://github.com/SSSD/sssd-test-framework/pull/247 moves this funtionality +# into the sssd-test-framework +def generate_ec_cert( + host, + key_path: str = "/tmp/ec_selfsigned.key", + cert_path: str = "/tmp/ec_selfsigned.crt", + subj: str = "/CN=Test EC Cert", +) -> tuple[str, str]: + """ + Generates a self-signed EC certificate and private key. + + :param key_path: Output path for the private key, defaults to "/tmp/ec_selfsigned.key" + :type key_path: str, optional + :param cert_path: Output path for the certificate, defaults to "/tmp/ec_selfsigned.crt" + :type cert_path: str, optional + :param subj: Subject for the certificate, defaults to "/CN=Test EC Cert" + :type subj: str, optional + :return: Tuple of (key_path, cert_path) + :rtype: tuple + """ + + host.conn.run( + f"openssl genpkey -algorithm EC -out {key_path} " + "-pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve" + ) + host.conn.run(f"openssl req -x509 -nodes -days 365 -key {key_path} -out {cert_path} -subj '{subj}'") + return key_path, cert_path + @pytest.mark.importance("critical") @pytest.mark.topology(KnownTopology.Keycloak) @@ -112,3 +146,164 @@ def test_oidc_child__get_group_members(client: Client, keycloak: Keycloak): data = json.loads(out.stdout) assert data[0]["posixUsername"] == "user1" assert data[0]["posixObjectType"] == "user" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Keycloak) +def test_oidc_child__get_device_code(client: Client, keycloak: Keycloak): + """ + :title: Authenticate with default settings + :setup: + 1. no specific setup needed + :steps: + 1. Request device code with oidc_child + :expectedresults: + 1. oidc_child is successful and device code and other data is returned + :customerscenario: False + """ + + out = client.host.conn.run( + oidc_child_path + " " + args_get_device_code + " " + "--client-id=myclient --client-secret=ClientSecret123" + ) + data = json.loads(out.stdout_lines[0]) + assert "device_code" in data, "Missing device_code!" + assert "expires_in" in data, "Missing expires_in!" + assert "interval" in data, "Missing interval!" + + assert out.stdout_lines[1][:8] == "oauth2 {", "Second line does not start with 'oauth2 {'!" + data = json.loads(out.stdout_lines[1][7:]) + assert "verification_uri" in data, "Missing verification_uri!" + assert "user_code" in data, "Missing user_code" + + +@pytest.mark.parametrize("key_type", ["RSA", "EC"]) +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Keycloak) +def test_oidc_child__get_device_code_jwt(client: Client, keycloak: Keycloak, key_type: str): + """ + :title: Authenticate with default settings + :setup: + 1. Create certificate, key and PKCS#12 file + 2. Create JWT client in Keycloak with certificate + :steps: + 1. Request device code with oidc_child with JWT client authentication + :expectedresults: + 1. oidc_child is successful and device code and other data is returned + :customerscenario: False + """ + + p12_pwd = "Secret123" + p12_path = "/tmp/my.p12" + + if key_type == "RSA": + key, cert = client.smartcard.generate_cert() + else: + key, cert = generate_ec_cert(client.host) + + client.host.conn.run(f"openssl pkcs12 -export -password pass:{p12_pwd} -inkey {key} -in {cert} -out {p12_path}") + out = client.host.conn.run(f"openssl x509 -in {cert} -outform der | openssl base64 -A") + cert_b64 = out.stdout + + # Create an IdP JWT client + keycloak.host.kclogin() + keycloak.host.conn.run( + "/opt/keycloak/bin/kcadm.sh create clients -r master " + '-b \'{"clientId": "my_jwt_client", "clientAuthenticatorType": "client-jwt", ' + '"serviceAccountsEnabled": true, ' + '"attributes": {"oauth2.device.authorization.grant.enabled": "true", ' + f'"jwt.credential.certificate": "{cert_b64}"}}}}\' ' + ) + + out = client.host.conn.run( + oidc_child_path + + " " + + args_get_device_code + + " " + + f"--client-id=my_jwt_client --client-secret={p12_pwd} --pkcs12-client-creds={p12_path} " + + "--client-auth-method=jwt" + ) + data = json.loads(out.stdout_lines[0]) + assert "device_code" in data, "Missing device_code!" + assert "expires_in" in data, "Missing expires_in!" + assert "interval" in data, "Missing interval!" + + assert out.stdout_lines[1][:8] == "oauth2 {", "Second line does not start with 'oauth2 {'!" + data = json.loads(out.stdout_lines[1][7:]) + assert "verification_uri" in data, "Missing verification_uri!" + assert "user_code" in data, "Missing user_code" + + +@pytest.mark.parametrize("key_type", ["RSA", "EC"]) +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Keycloak) +def test_oidc_child__get_device_code_mtls(client: Client, keycloak: Keycloak, key_type: str): + """ + :title: Authenticate with default settings + :setup: + 1. Create certificate, key and PKCS#12 file + 2. Let keycloak trust the client certificate + 3. Enable HTTPS client authentication in Keycloak + 4. Create MTLS client in Keycloak with the subject DN of the certificate + :steps: + 1. Request device code with oidc_child with MTLS client authentication + :expectedresults: + 1. oidc_child is successful and device code and other data is returned + :customerscenario: False + """ + + p12_pwd = "Secret123" + p12_path = "/tmp/my.p12" + + if key_type == "RSA": + key, cert = client.smartcard.generate_cert() + else: + key, cert = generate_ec_cert(client.host) + + client.host.conn.run(f"openssl pkcs12 -export -password pass:{p12_pwd} -inkey {key} -in {cert} -out {p12_path}") + out = client.host.conn.run(f"openssl x509 -in {cert} -noout -subject") + assert out.stdout[:8] == "subject=", "Unexpected output!" + subject_dn = out.stdout[8:] + cert_content = client.host.fs.read(cert) + + # Add client certificate to Keycloak's kestore and enable HTTPS client + # authentication + keycloak.fs.write(cert, cert_content) + keycloak.host.conn.run( + "keytool -storepass Secret123 -keystore /var/data/certs/master.keycloak.test.keystore -noprompt " + + f"-importcert -file {cert} -alias mtls" + ) + # https://github.com/SSSD/sssd-ci-containers/pull/179 will set this by default + keycloak.host.conn.run("echo KC_HTTPS_CLIENT_AUTH=request >> /etc/keycloak.env") + keycloak.svc.restart("keycloak.service") + + # Create an IdP MTLS client + keycloak.host.kclogin() + keycloak.host.conn.run( + "/opt/keycloak/bin/kcadm.sh create clients -r master " + '-b \'{"clientId": "my_mtls_client", "clientAuthenticatorType": "client-x509", ' + '"serviceAccountsEnabled": true, ' + '"attributes": {"oauth2.device.authorization.grant.enabled": "true", ' + f'"x509.subjectdn": "{subject_dn}"}}}}\' ' + ) + + out = client.host.conn.run( + oidc_child_path + + " " + + args_get_device_code + + " " + + f"--client-id=my_mtls_client --client-secret={p12_pwd} --pkcs12-client-creds={p12_path} " + + "--client-auth-method=mtls" + ) + # Currently the keystore is not restored by the framework + keycloak.host.conn.run( + "keytool -storepass Secret123 -keystore /var/data/certs/master.keycloak.test.keystore -delete -alias mtls" + ) + data = json.loads(out.stdout_lines[0]) + assert "device_code" in data, "Missing device_code!" + assert "expires_in" in data, "Missing expires_in!" + assert "interval" in data, "Missing interval!" + + assert out.stdout_lines[1][:8] == "oauth2 {", "Second line does not start with 'oauth2 {'!" + data = json.loads(out.stdout_lines[1][7:]) + assert "verification_uri" in data, "Missing verification_uri!" + assert "user_code" in data, "Missing user_code"