|
|
ced1f5 |
From e857005c207e514c487ba75daf20ca4be5321c38 Mon Sep 17 00:00:00 2001
|
|
|
ced1f5 |
From: Sumit Bose <sbose@redhat.com>
|
|
|
ced1f5 |
Date: Fri, 27 Oct 2017 10:13:36 +0200
|
|
|
ced1f5 |
Subject: [PATCH 39/46] p11_child: use options to select certificate for
|
|
|
ced1f5 |
authentication
|
|
|
ced1f5 |
MIME-Version: 1.0
|
|
|
ced1f5 |
Content-Type: text/plain; charset=UTF-8
|
|
|
ced1f5 |
Content-Transfer-Encoding: 8bit
|
|
|
ced1f5 |
|
|
|
ced1f5 |
New options are added to p11_child to select a specific certificate
|
|
|
ced1f5 |
during authentication.
|
|
|
ced1f5 |
|
|
|
ced1f5 |
The related unit tests are updated by adding the needed attributes to
|
|
|
ced1f5 |
the requests. The was not necessary before because although the
|
|
|
ced1f5 |
attribute were already send by pam_sss they were not used in the PAM
|
|
|
ced1f5 |
responder but only forwarded to the back where they were used by the
|
|
|
ced1f5 |
PKINIT code to select the expected certificate.
|
|
|
ced1f5 |
|
|
|
ced1f5 |
Related to https://pagure.io/SSSD/sssd/issue/3560
|
|
|
ced1f5 |
|
|
|
ced1f5 |
Reviewed-by: Fabiano FidĂȘncio <fidencio@redhat.com>
|
|
|
ced1f5 |
Tested-by: Scott Poore <spoore@redhat.com>
|
|
|
ced1f5 |
(cherry picked from commit 0a8024af282b271ad2185f68703d9f4e766d2bdc)
|
|
|
ced1f5 |
---
|
|
|
ced1f5 |
src/p11_child/p11_child_nss.c | 213 ++++++++++++++++++++++++++--------------
|
|
|
ced1f5 |
src/responder/pam/pamsrv_p11.c | 30 +++++-
|
|
|
ced1f5 |
src/tests/cmocka/test_pam_srv.c | 64 ++++++++----
|
|
|
ced1f5 |
3 files changed, 210 insertions(+), 97 deletions(-)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
diff --git a/src/p11_child/p11_child_nss.c b/src/p11_child/p11_child_nss.c
|
|
|
ced1f5 |
index 50bde2f4f91f6c00260b0db383d0962112686ebc..c676375cf7f6677a1d7f38f09b9bb5fd820d60c5 100644
|
|
|
ced1f5 |
--- a/src/p11_child/p11_child_nss.c
|
|
|
ced1f5 |
+++ b/src/p11_child/p11_child_nss.c
|
|
|
ced1f5 |
@@ -67,12 +67,34 @@ static char *password_passthrough(PK11SlotInfo *slot, PRBool retry, void *arg)
|
|
|
ced1f5 |
return PL_strdup((char *)arg);
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
+static char *get_key_id_str(PK11SlotInfo *slot, CERTCertificate *cert)
|
|
|
ced1f5 |
+{
|
|
|
ced1f5 |
+ SECItem *key_id = NULL;
|
|
|
ced1f5 |
+ char *key_id_str = NULL;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
+ key_id = PK11_GetLowLevelKeyIDForCert(slot, cert, NULL);
|
|
|
ced1f5 |
+ if (key_id == NULL) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE,
|
|
|
ced1f5 |
+ "PK11_GetLowLevelKeyIDForCert failed [%d].\n",
|
|
|
ced1f5 |
+ PR_GetError());
|
|
|
ced1f5 |
+ return NULL;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
|
|
|
ced1f5 |
-int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
+ key_id_str = CERT_Hexify(key_id, PR_FALSE);
|
|
|
ced1f5 |
+ SECITEM_FreeItem(key_id, PR_TRUE);
|
|
|
ced1f5 |
+ if (key_id_str == NULL) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE, "CERT_Hexify failed [%d].\n", PR_GetError());
|
|
|
ced1f5 |
+ return NULL;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ return key_id_str;
|
|
|
ced1f5 |
+}
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+int do_work(TALLOC_CTX *mem_ctx, const char *nss_db,
|
|
|
ced1f5 |
enum op_mode mode, const char *pin,
|
|
|
ced1f5 |
struct cert_verify_opts *cert_verify_opts,
|
|
|
ced1f5 |
- char **_multi)
|
|
|
ced1f5 |
+ const char *module_name_in, const char *token_name_in,
|
|
|
ced1f5 |
+ const char *key_id_in, char **_multi)
|
|
|
ced1f5 |
{
|
|
|
ced1f5 |
int ret;
|
|
|
ced1f5 |
SECStatus rv;
|
|
|
ced1f5 |
@@ -153,42 +175,31 @@ int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
mod_list_item->module->dllName);
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- if (slot_name_in != NULL) {
|
|
|
ced1f5 |
- slot = PK11_FindSlotByName(slot_name_in);
|
|
|
ced1f5 |
- if (slot == NULL) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_OP_FAILURE, "PK11_FindSlotByName failed for [%s]: [%d].\n",
|
|
|
ced1f5 |
- slot_name_in, PR_GetError());
|
|
|
ced1f5 |
- return EIO;
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
- } else {
|
|
|
ced1f5 |
-
|
|
|
ced1f5 |
- list = PK11_GetAllTokens(CKM_INVALID_MECHANISM, PR_FALSE, PR_TRUE,
|
|
|
ced1f5 |
- NULL);
|
|
|
ced1f5 |
- if (list == NULL) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_OP_FAILURE, "PK11_GetAllTokens failed.\n");
|
|
|
ced1f5 |
- return EIO;
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
+ list = PK11_GetAllTokens(CKM_INVALID_MECHANISM, PR_FALSE, PR_TRUE,
|
|
|
ced1f5 |
+ NULL);
|
|
|
ced1f5 |
+ if (list == NULL) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE, "PK11_GetAllTokens failed.\n");
|
|
|
ced1f5 |
+ return EIO;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- for (le = list->head; le; le = le->next) {
|
|
|
ced1f5 |
- CK_SLOT_INFO slInfo;
|
|
|
ced1f5 |
+ for (le = list->head; le; le = le->next) {
|
|
|
ced1f5 |
+ CK_SLOT_INFO slInfo;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- slInfo.flags = 0;
|
|
|
ced1f5 |
- rv = PK11_GetSlotInfo(le->slot, &slInfo);
|
|
|
ced1f5 |
- DEBUG(SSSDBG_TRACE_ALL,
|
|
|
ced1f5 |
- "Description [%s] Manufacturer [%s] flags [%lu].\n",
|
|
|
ced1f5 |
- slInfo.slotDescription, slInfo.manufacturerID, slInfo.flags);
|
|
|
ced1f5 |
- if (rv == SECSuccess && (slInfo.flags & CKF_REMOVABLE_DEVICE)) {
|
|
|
ced1f5 |
- slot = PK11_ReferenceSlot(le->slot);
|
|
|
ced1f5 |
- break;
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
- PK11_FreeSlotList(list);
|
|
|
ced1f5 |
- if (slot == NULL) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_OP_FAILURE, "No removable slots found.\n");
|
|
|
ced1f5 |
- return EIO;
|
|
|
ced1f5 |
+ slInfo.flags = 0;
|
|
|
ced1f5 |
+ rv = PK11_GetSlotInfo(le->slot, &slInfo);
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_TRACE_ALL,
|
|
|
ced1f5 |
+ "Description [%s] Manufacturer [%s] flags [%lu].\n",
|
|
|
ced1f5 |
+ slInfo.slotDescription, slInfo.manufacturerID, slInfo.flags);
|
|
|
ced1f5 |
+ if (rv == SECSuccess && (slInfo.flags & CKF_REMOVABLE_DEVICE)) {
|
|
|
ced1f5 |
+ slot = PK11_ReferenceSlot(le->slot);
|
|
|
ced1f5 |
+ break;
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
-
|
|
|
ced1f5 |
+ PK11_FreeSlotList(list);
|
|
|
ced1f5 |
+ if (slot == NULL) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE, "No removable slots found.\n");
|
|
|
ced1f5 |
+ return EIO;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
|
|
|
ced1f5 |
slot_id = PK11_GetSlotID(slot);
|
|
|
ced1f5 |
module_id = PK11_GetModuleID(slot);
|
|
|
ced1f5 |
@@ -317,24 +328,60 @@ int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
for (cert_list_node = CERT_LIST_HEAD(cert_list);
|
|
|
ced1f5 |
!CERT_LIST_END(cert_list_node, cert_list);
|
|
|
ced1f5 |
cert_list_node = CERT_LIST_NEXT(cert_list_node)) {
|
|
|
ced1f5 |
- if (cert_list_node->cert) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_TRACE_ALL, "found cert[%s][%s]\n",
|
|
|
ced1f5 |
- cert_list_node->cert->nickname,
|
|
|
ced1f5 |
- cert_list_node->cert->subjectName);
|
|
|
ced1f5 |
+ if (cert_list_node->cert == NULL) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_TRACE_ALL, "--- empty cert list node ---\n");
|
|
|
ced1f5 |
+ continue;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- if (cert_verify_opts->do_verification) {
|
|
|
ced1f5 |
- rv = CERT_VerifyCertificateNow(handle, cert_list_node->cert,
|
|
|
ced1f5 |
- PR_TRUE,
|
|
|
ced1f5 |
- certificateUsageSSLClient,
|
|
|
ced1f5 |
- NULL, NULL);
|
|
|
ced1f5 |
- if (rv != SECSuccess) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_OP_FAILURE,
|
|
|
ced1f5 |
- "Certificate [%s][%s] not valid [%d], skipping.\n",
|
|
|
ced1f5 |
- cert_list_node->cert->nickname,
|
|
|
ced1f5 |
- cert_list_node->cert->subjectName, PR_GetError());
|
|
|
ced1f5 |
- continue;
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_TRACE_ALL,
|
|
|
ced1f5 |
+ "found cert[%s][%s]\n",
|
|
|
ced1f5 |
+ cert_list_node->cert->nickname,
|
|
|
ced1f5 |
+ cert_list_node->cert->subjectName);
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ if (cert_verify_opts->do_verification) {
|
|
|
ced1f5 |
+ rv = CERT_VerifyCertificateNow(handle, cert_list_node->cert,
|
|
|
ced1f5 |
+ PR_TRUE,
|
|
|
ced1f5 |
+ certificateUsageSSLClient,
|
|
|
ced1f5 |
+ NULL, NULL);
|
|
|
ced1f5 |
+ if (rv != SECSuccess) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE,
|
|
|
ced1f5 |
+ "Certificate [%s][%s] not valid [%d], skipping.\n",
|
|
|
ced1f5 |
+ cert_list_node->cert->nickname,
|
|
|
ced1f5 |
+ cert_list_node->cert->subjectName, PR_GetError());
|
|
|
ced1f5 |
+ continue;
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ if (key_id_in != NULL) {
|
|
|
ced1f5 |
+ PORT_Free(key_id_str);
|
|
|
ced1f5 |
+ key_id_str = NULL;
|
|
|
ced1f5 |
+ key_id_str = get_key_id_str(slot, cert_list_node->cert);
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+ /* Check if we found the certificates we needed for authentication or
|
|
|
ced1f5 |
+ * the requested ones for pre-auth. For authentication all attributes
|
|
|
ced1f5 |
+ * must be given and match, for pre-auth only the given ones must
|
|
|
ced1f5 |
+ * match. */
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_TRACE_ALL, "%s %s %s %s %s %s.\n",
|
|
|
ced1f5 |
+ module_name_in, module_name, token_name_in, token_name,
|
|
|
ced1f5 |
+ key_id_in, key_id_str);
|
|
|
ced1f5 |
+ if ((mode == OP_AUTH
|
|
|
ced1f5 |
+ && module_name_in != NULL
|
|
|
ced1f5 |
+ && token_name_in != NULL
|
|
|
ced1f5 |
+ && key_id_in != NULL
|
|
|
ced1f5 |
+ && key_id_str != NULL
|
|
|
ced1f5 |
+ && strcmp(key_id_in, key_id_str) == 0
|
|
|
ced1f5 |
+ && strcmp(token_name_in, token_name) == 0
|
|
|
ced1f5 |
+ && strcmp(module_name_in, module_name) == 0)
|
|
|
ced1f5 |
+ || (mode == OP_PREAUTH
|
|
|
ced1f5 |
+ && (module_name_in == NULL
|
|
|
ced1f5 |
+ || (module_name_in != NULL
|
|
|
ced1f5 |
+ && strcmp(module_name_in, module_name) == 0))
|
|
|
ced1f5 |
+ && (token_name_in == NULL
|
|
|
ced1f5 |
+ || (token_name_in != NULL
|
|
|
ced1f5 |
+ && strcmp(token_name_in, token_name) == 0))
|
|
|
ced1f5 |
+ && (key_id_in == NULL
|
|
|
ced1f5 |
+ || (key_id_in != NULL && key_id_str != NULL
|
|
|
ced1f5 |
+ && strcmp(key_id_in, key_id_str) == 0)))) {
|
|
|
ced1f5 |
|
|
|
ced1f5 |
rv = CERT_AddCertToListTail(valid_certs, cert_list_node->cert);
|
|
|
ced1f5 |
if (rv != SECSuccess) {
|
|
|
ced1f5 |
@@ -343,15 +390,6 @@ int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
ret = EIO;
|
|
|
ced1f5 |
goto done;
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
-
|
|
|
ced1f5 |
- if (found_cert == NULL) {
|
|
|
ced1f5 |
- found_cert = cert_list_node->cert;
|
|
|
ced1f5 |
- } else {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_TRACE_ALL, "More than one certificate found, " \
|
|
|
ced1f5 |
- "using just the first one.\n");
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
- } else {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_TRACE_ALL, "--- empty cert list node ---\n");
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
@@ -367,7 +405,7 @@ int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- if (found_cert == NULL) {
|
|
|
ced1f5 |
+ if (CERT_LIST_EMPTY(valid_certs)) {
|
|
|
ced1f5 |
DEBUG(SSSDBG_TRACE_ALL, "No certificate found.\n");
|
|
|
ced1f5 |
*_multi = NULL;
|
|
|
ced1f5 |
ret = EOK;
|
|
|
ced1f5 |
@@ -375,6 +413,23 @@ int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
if (mode == OP_AUTH) {
|
|
|
ced1f5 |
+ cert_list_node = CERT_LIST_HEAD(valid_certs);
|
|
|
ced1f5 |
+ if (!CERT_LIST_END(CERT_LIST_NEXT(cert_list_node), valid_certs)) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_FATAL_FAILURE,
|
|
|
ced1f5 |
+ "More than one certificate found for authentication, "
|
|
|
ced1f5 |
+ "aborting!\n");
|
|
|
ced1f5 |
+ ret = EINVAL;
|
|
|
ced1f5 |
+ goto done;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ found_cert = cert_list_node->cert;
|
|
|
ced1f5 |
+ if (found_cert == NULL) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_FATAL_FAILURE,
|
|
|
ced1f5 |
+ "No certificate found for authentication, aborting!\n");
|
|
|
ced1f5 |
+ ret = EINVAL;
|
|
|
ced1f5 |
+ goto done;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
rv = PK11_GenerateRandom(random_value, sizeof(random_value));
|
|
|
ced1f5 |
if (rv != SECSuccess) {
|
|
|
ced1f5 |
DEBUG(SSSDBG_OP_FAILURE,
|
|
|
ced1f5 |
@@ -449,21 +504,10 @@ int do_work(TALLOC_CTX *mem_ctx, const char *nss_db, const char *slot_name_in,
|
|
|
ced1f5 |
|
|
|
ced1f5 |
found_cert = cert_list_node->cert;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- SECITEM_FreeItem(key_id, PR_TRUE);
|
|
|
ced1f5 |
PORT_Free(key_id_str);
|
|
|
ced1f5 |
- key_id = PK11_GetLowLevelKeyIDForCert(slot, found_cert, NULL);
|
|
|
ced1f5 |
- if (key_id == NULL) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_OP_FAILURE,
|
|
|
ced1f5 |
- "PK11_GetLowLevelKeyIDForCert failed [%d].\n",
|
|
|
ced1f5 |
- PR_GetError());
|
|
|
ced1f5 |
- ret = EINVAL;
|
|
|
ced1f5 |
- goto done;
|
|
|
ced1f5 |
- }
|
|
|
ced1f5 |
-
|
|
|
ced1f5 |
- key_id_str = CERT_Hexify(key_id, PR_FALSE);
|
|
|
ced1f5 |
+ key_id_str = get_key_id_str(slot, found_cert);
|
|
|
ced1f5 |
if (key_id_str == NULL) {
|
|
|
ced1f5 |
- DEBUG(SSSDBG_OP_FAILURE, "CERT_Hexify failed [%d].\n",
|
|
|
ced1f5 |
- PR_GetError());
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE, "get_key_id_str [%d].\n", PR_GetError());
|
|
|
ced1f5 |
ret = ENOMEM;
|
|
|
ced1f5 |
goto done;
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
@@ -576,11 +620,13 @@ int main(int argc, const char *argv[])
|
|
|
ced1f5 |
enum op_mode mode = OP_NONE;
|
|
|
ced1f5 |
enum pin_mode pin_mode = PIN_NONE;
|
|
|
ced1f5 |
char *pin = NULL;
|
|
|
ced1f5 |
- char *slot_name_in = NULL;
|
|
|
ced1f5 |
char *nss_db = NULL;
|
|
|
ced1f5 |
struct cert_verify_opts *cert_verify_opts;
|
|
|
ced1f5 |
char *verify_opts = NULL;
|
|
|
ced1f5 |
char *multi = NULL;
|
|
|
ced1f5 |
+ char *module_name = NULL;
|
|
|
ced1f5 |
+ char *token_name = NULL;
|
|
|
ced1f5 |
+ char *key_id = NULL;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
struct poptOption long_options[] = {
|
|
|
ced1f5 |
POPT_AUTOHELP
|
|
|
ced1f5 |
@@ -605,6 +651,12 @@ int main(int argc, const char *argv[])
|
|
|
ced1f5 |
NULL},
|
|
|
ced1f5 |
{"nssdb", 0, POPT_ARG_STRING, &nss_db, 0, _("NSS DB to use"),
|
|
|
ced1f5 |
NULL},
|
|
|
ced1f5 |
+ {"module_name", 0, POPT_ARG_STRING, &module_name, 0,
|
|
|
ced1f5 |
+ _("Module name for authentication"), NULL},
|
|
|
ced1f5 |
+ {"token_name", 0, POPT_ARG_STRING, &token_name, 0,
|
|
|
ced1f5 |
+ _("Token name for authentication"), NULL},
|
|
|
ced1f5 |
+ {"key_id", 0, POPT_ARG_STRING, &key_id, 0,
|
|
|
ced1f5 |
+ _("Key ID for authentication"), NULL},
|
|
|
ced1f5 |
POPT_TABLEEND
|
|
|
ced1f5 |
};
|
|
|
ced1f5 |
|
|
|
ced1f5 |
@@ -730,6 +782,15 @@ int main(int argc, const char *argv[])
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
talloc_steal(main_ctx, debug_prg_name);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
+ if (mode == OP_AUTH && (module_name == NULL || token_name == NULL
|
|
|
ced1f5 |
+ || key_id == NULL)) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_FATAL_FAILURE,
|
|
|
ced1f5 |
+ "--module_name, --token_name and --key_id must be for "
|
|
|
ced1f5 |
+ "authentication");
|
|
|
ced1f5 |
+ ret = EINVAL;
|
|
|
ced1f5 |
+ goto fail;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
ret = parse_cert_verify_opts(main_ctx, verify_opts, &cert_verify_opts);
|
|
|
ced1f5 |
if (ret != EOK) {
|
|
|
ced1f5 |
DEBUG(SSSDBG_FATAL_FAILURE, "Failed to parse verifiy option.\n");
|
|
|
ced1f5 |
@@ -744,8 +805,8 @@ int main(int argc, const char *argv[])
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- ret = do_work(main_ctx, nss_db, slot_name_in, mode, pin, cert_verify_opts,
|
|
|
ced1f5 |
- &multi);
|
|
|
ced1f5 |
+ ret = do_work(main_ctx, nss_db, mode, pin, cert_verify_opts, module_name,
|
|
|
ced1f5 |
+ token_name, key_id, &multi);
|
|
|
ced1f5 |
if (ret != EOK) {
|
|
|
ced1f5 |
DEBUG(SSSDBG_OP_FAILURE, "do_work failed.\n");
|
|
|
ced1f5 |
goto fail;
|
|
|
ced1f5 |
diff --git a/src/responder/pam/pamsrv_p11.c b/src/responder/pam/pamsrv_p11.c
|
|
|
ced1f5 |
index 57c8e1e464f4262f2d78f869c52ca48bd469d90a..4d5572164763ed0b3a842019f820680a4dc2dfdc 100644
|
|
|
ced1f5 |
--- a/src/responder/pam/pamsrv_p11.c
|
|
|
ced1f5 |
+++ b/src/responder/pam/pamsrv_p11.c
|
|
|
ced1f5 |
@@ -399,10 +399,13 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx,
|
|
|
ced1f5 |
struct timeval tv;
|
|
|
ced1f5 |
int pipefd_to_child[2] = PIPE_INIT;
|
|
|
ced1f5 |
int pipefd_from_child[2] = PIPE_INIT;
|
|
|
ced1f5 |
- const char *extra_args[7] = { NULL };
|
|
|
ced1f5 |
+ const char *extra_args[13] = { NULL };
|
|
|
ced1f5 |
uint8_t *write_buf = NULL;
|
|
|
ced1f5 |
size_t write_buf_len = 0;
|
|
|
ced1f5 |
size_t arg_c;
|
|
|
ced1f5 |
+ const char *module_name = NULL;
|
|
|
ced1f5 |
+ const char *token_name = NULL;
|
|
|
ced1f5 |
+ const char *key_id = NULL;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
req = tevent_req_create(mem_ctx, &state, struct pam_check_cert_state);
|
|
|
ced1f5 |
if (req == NULL) {
|
|
|
ced1f5 |
@@ -423,6 +426,30 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx,
|
|
|
ced1f5 |
extra_args[arg_c++] = verify_opts;
|
|
|
ced1f5 |
extra_args[arg_c++] = "--verify";
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ if (sss_authtok_get_type(pd->authtok) == SSS_AUTHTOK_TYPE_SC_PIN
|
|
|
ced1f5 |
+ || sss_authtok_get_type(pd->authtok) == SSS_AUTHTOK_TYPE_SC_KEYPAD) {
|
|
|
ced1f5 |
+ ret = sss_authtok_get_sc(pd->authtok, NULL, NULL, &token_name, NULL,
|
|
|
ced1f5 |
+ &module_name, NULL, &key_id, NULL);
|
|
|
ced1f5 |
+ if (ret != EOK) {
|
|
|
ced1f5 |
+ DEBUG(SSSDBG_OP_FAILURE, "sss_authtok_get_sc failed.\n");
|
|
|
ced1f5 |
+ goto done;
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ if (module_name != NULL && *module_name != '\0') {
|
|
|
ced1f5 |
+ extra_args[arg_c++] = module_name;
|
|
|
ced1f5 |
+ extra_args[arg_c++] = "--module_name";
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+ if (token_name != NULL && *token_name != '\0') {
|
|
|
ced1f5 |
+ extra_args[arg_c++] = token_name;
|
|
|
ced1f5 |
+ extra_args[arg_c++] = "--token_name";
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+ if (key_id != NULL && *key_id != '\0') {
|
|
|
ced1f5 |
+ extra_args[arg_c++] = key_id;
|
|
|
ced1f5 |
+ extra_args[arg_c++] = "--key_id";
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+ }
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
if (pd->cmd == SSS_PAM_AUTHENTICATE) {
|
|
|
ced1f5 |
extra_args[arg_c++] = "--auth";
|
|
|
ced1f5 |
switch (sss_authtok_get_type(pd->authtok)) {
|
|
|
ced1f5 |
@@ -437,6 +464,7 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx,
|
|
|
ced1f5 |
ret = EINVAL;
|
|
|
ced1f5 |
goto done;
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
} else if (pd->cmd == SSS_PAM_PREAUTH) {
|
|
|
ced1f5 |
extra_args[arg_c++] = "--pre";
|
|
|
ced1f5 |
} else {
|
|
|
ced1f5 |
diff --git a/src/tests/cmocka/test_pam_srv.c b/src/tests/cmocka/test_pam_srv.c
|
|
|
ced1f5 |
index 7f0ed706512ffe0866c0e1fb7e6baa16bec942d8..5c1f621ccead75717d1721714d953d7d4d415d7b 100644
|
|
|
ced1f5 |
--- a/src/tests/cmocka/test_pam_srv.c
|
|
|
ced1f5 |
+++ b/src/tests/cmocka/test_pam_srv.c
|
|
|
ced1f5 |
@@ -687,7 +687,9 @@ static void mock_input_pam(TALLOC_CTX *mem_ctx,
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
static void mock_input_pam_cert(TALLOC_CTX *mem_ctx, const char *name,
|
|
|
ced1f5 |
- const char *pin, const char *service,
|
|
|
ced1f5 |
+ const char *pin, const char *token_name,
|
|
|
ced1f5 |
+ const char *module_name, const char *key_id,
|
|
|
ced1f5 |
+ const char *service,
|
|
|
ced1f5 |
acct_cb_t acct_cb, const char *cert,
|
|
|
ced1f5 |
bool only_one_provider_call)
|
|
|
ced1f5 |
{
|
|
|
ced1f5 |
@@ -697,6 +699,7 @@ static void mock_input_pam_cert(TALLOC_CTX *mem_ctx, const char *name,
|
|
|
ced1f5 |
struct pam_items pi = { 0 };
|
|
|
ced1f5 |
int ret;
|
|
|
ced1f5 |
bool already_mocked = false;
|
|
|
ced1f5 |
+ size_t needed_size;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
if (name != NULL) {
|
|
|
ced1f5 |
pi.pam_user = name;
|
|
|
ced1f5 |
@@ -707,9 +710,21 @@ static void mock_input_pam_cert(TALLOC_CTX *mem_ctx, const char *name,
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
if (pin != NULL) {
|
|
|
ced1f5 |
- pi.pam_authtok = discard_const(pin);
|
|
|
ced1f5 |
- pi.pam_authtok_size = strlen(pi.pam_authtok) + 1;
|
|
|
ced1f5 |
+ ret = sss_auth_pack_sc_blob(pin, 0, token_name, 0, module_name, 0,
|
|
|
ced1f5 |
+ key_id, 0, NULL, 0, &needed_size);
|
|
|
ced1f5 |
+ assert_int_equal(ret, EAGAIN);
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ pi.pam_authtok = malloc(needed_size);
|
|
|
ced1f5 |
+ assert_non_null(pi.pam_authtok);
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
+ ret = sss_auth_pack_sc_blob(pin, 0, token_name, 0, module_name, 0,
|
|
|
ced1f5 |
+ key_id, 0,
|
|
|
ced1f5 |
+ (uint8_t *)pi.pam_authtok, needed_size,
|
|
|
ced1f5 |
+ &needed_size);
|
|
|
ced1f5 |
+ assert_int_equal(ret, EOK);
|
|
|
ced1f5 |
+
|
|
|
ced1f5 |
pi.pam_authtok_type = SSS_AUTHTOK_TYPE_SC_PIN;
|
|
|
ced1f5 |
+ pi.pam_authtok_size = needed_size;
|
|
|
ced1f5 |
}
|
|
|
ced1f5 |
|
|
|
ced1f5 |
pi.pam_service = service == NULL ? "login" : service;
|
|
|
ced1f5 |
@@ -724,6 +739,7 @@ static void mock_input_pam_cert(TALLOC_CTX *mem_ctx, const char *name,
|
|
|
ced1f5 |
pi.cli_pid = 12345;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
ret = pack_message_v3(&pi, &buf_size, &m_buf);
|
|
|
ced1f5 |
+ free(pi.pam_authtok);
|
|
|
ced1f5 |
assert_int_equal(ret, 0);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
buf = talloc_memdup(mem_ctx, m_buf, buf_size);
|
|
|
ced1f5 |
@@ -1732,7 +1748,8 @@ void test_pam_preauth_no_logon_name(void **state)
|
|
|
ced1f5 |
{
|
|
|
ced1f5 |
int ret;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, false);
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ NULL, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_body, WRAP_CALL_REAL);
|
|
|
ced1f5 |
@@ -1824,7 +1841,8 @@ void test_pam_preauth_cert_nocert(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, "/no/path");
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, false);
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ NULL, NULL, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_body, WRAP_CALL_REAL);
|
|
|
ced1f5 |
@@ -1962,7 +1980,7 @@ void test_pam_preauth_cert_nomatch(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, NULL, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -1984,7 +2002,7 @@ void test_pam_preauth_cert_match(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -2007,8 +2025,9 @@ void test_pam_preauth_cert_match_gdm_smartcard(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, "gdm-smartcard",
|
|
|
ced1f5 |
- test_lookup_by_cert_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ "gdm-smartcard", test_lookup_by_cert_cb,
|
|
|
ced1f5 |
+ TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_body, WRAP_CALL_REAL);
|
|
|
ced1f5 |
@@ -2029,7 +2048,7 @@ void test_pam_preauth_cert_match_wrong_user(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_wrong_user_cb,
|
|
|
ced1f5 |
TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
@@ -2061,7 +2080,7 @@ void test_pam_preauth_cert_no_logon_name(void **state)
|
|
|
ced1f5 |
* Additionally sss_parse_inp_recv() must be mocked because the cache
|
|
|
ced1f5 |
* request will be done with the username found by the certificate
|
|
|
ced1f5 |
* lookup. */
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
mock_account_recv_simple();
|
|
|
ced1f5 |
mock_parse_inp("pamuser", NULL, EOK);
|
|
|
ced1f5 |
@@ -2090,7 +2109,7 @@ void test_pam_preauth_cert_no_logon_name_with_hint(void **state)
|
|
|
ced1f5 |
* Since user name hint is enabled we do not have to search the user
|
|
|
ced1f5 |
* during pre-auth and there is no need for an extra mocked response as in
|
|
|
ced1f5 |
* test_pam_preauth_cert_no_logon_name. */
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -2112,7 +2131,7 @@ void test_pam_preauth_cert_no_logon_name_double_cert(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_double_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -2135,7 +2154,7 @@ void test_pam_preauth_cert_no_logon_name_double_cert_with_hint(void **state)
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
pam_test_ctx->rctx->domains->user_name_hint = true;
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_double_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -2157,7 +2176,8 @@ void test_pam_preauth_no_cert_no_logon_name(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, "/no/path");
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, false);
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ NULL, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_body, WRAP_CALL_REAL);
|
|
|
ced1f5 |
@@ -2178,7 +2198,7 @@ void test_pam_preauth_cert_no_logon_name_no_match(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, NULL, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -2206,7 +2226,9 @@ void test_pam_cert_auth(void **state)
|
|
|
ced1f5 |
* is looked up. Since the first mocked reply already adds the certificate
|
|
|
ced1f5 |
* to the user entry the lookup by certificate will already find the user
|
|
|
ced1f5 |
* in the cache and no second request to the backend is needed. */
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", "123456", NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", "123456", "SSSD Test Token",
|
|
|
ced1f5 |
+ "NSS-Internal",
|
|
|
ced1f5 |
+ "A5EF7DEE625CA5996C8D1BA7D036708161FD49E7", NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, TEST_TOKEN_CERT, true);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_AUTHENTICATE);
|
|
|
ced1f5 |
@@ -2232,7 +2254,9 @@ void test_pam_cert_auth_double_cert(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", "123456", NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", "123456", "SSSD Test Token",
|
|
|
ced1f5 |
+ "NSS-Internal",
|
|
|
ced1f5 |
+ "A5EF7DEE625CA5996C8D1BA7D036708161FD49E7", NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_double_cb, TEST_TOKEN_CERT, true);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_AUTHENTICATE);
|
|
|
ced1f5 |
@@ -2257,7 +2281,7 @@ void test_pam_cert_preauth_2certs_one_mapping(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB_2CERTS);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb, TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
will_return(__wrap_sss_packet_get_cmd, SSS_PAM_PREAUTH);
|
|
|
ced1f5 |
@@ -2279,7 +2303,7 @@ void test_pam_cert_preauth_2certs_two_mappings(void **state)
|
|
|
ced1f5 |
|
|
|
ced1f5 |
set_cert_auth_param(pam_test_ctx->pctx, NSS_DB_2CERTS);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
- mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL,
|
|
|
ced1f5 |
+ mock_input_pam_cert(pam_test_ctx, "pamuser", NULL, NULL, NULL, NULL, NULL,
|
|
|
ced1f5 |
test_lookup_by_cert_cb_2nd_cert_same_user,
|
|
|
ced1f5 |
TEST_TOKEN_CERT, false);
|
|
|
ced1f5 |
|
|
|
ced1f5 |
--
|
|
|
ced1f5 |
2.13.6
|
|
|
ced1f5 |
|