|
|
6cf099 |
From ace8a2f80fe5bb755512e4a7281146ce4fe311a6 Mon Sep 17 00:00:00 2001
|
|
|
6cf099 |
From: Sumit Bose <sbose@redhat.com>
|
|
|
6cf099 |
Date: Wed, 15 Jul 2015 09:40:00 +0200
|
|
|
6cf099 |
Subject: [PATCH 36/37] ssh: generate public keys from certificate
|
|
|
6cf099 |
|
|
|
6cf099 |
Resolves: https://fedorahosted.org/sssd/ticket/2711
|
|
|
6cf099 |
|
|
|
6cf099 |
Reviewed-by: Jakub Hrozek <jhrozek@redhat.com>
|
|
|
6cf099 |
---
|
|
|
6cf099 |
Makefile.am | 7 +-
|
|
|
6cf099 |
src/confdb/confdb.h | 2 +
|
|
|
6cf099 |
src/config/SSSDConfig/__init__.py.in | 1 +
|
|
|
6cf099 |
src/config/etc/sssd.api.conf | 1 +
|
|
|
6cf099 |
src/man/sssd.conf.5.xml | 13 ++++
|
|
|
6cf099 |
src/responder/ssh/sshsrv.c | 9 +++
|
|
|
6cf099 |
src/responder/ssh/sshsrv_cmd.c | 54 ++++++++++++---
|
|
|
6cf099 |
src/responder/ssh/sshsrv_private.h | 1 +
|
|
|
6cf099 |
src/tests/cmocka/test_cert_utils.c | 62 +++++++++++++++++
|
|
|
6cf099 |
src/util/cert.h | 4 ++
|
|
|
6cf099 |
src/util/cert/libcrypto/cert.c | 93 +++++++++++++++++++++++++
|
|
|
6cf099 |
src/util/cert/nss/cert.c | 130 +++++++++++++++++++++++++++++++++++
|
|
|
6cf099 |
12 files changed, 364 insertions(+), 13 deletions(-)
|
|
|
6cf099 |
|
|
|
6cf099 |
diff --git a/Makefile.am b/Makefile.am
|
|
|
6cf099 |
index fd78c1bb770b98adee9a6c1ead3b0d6f4345fb9b..5345d90d22cd285a5268ac50a6b527645acdb351 100644
|
|
|
6cf099 |
--- a/Makefile.am
|
|
|
6cf099 |
+++ b/Makefile.am
|
|
|
6cf099 |
@@ -1129,10 +1129,13 @@ sssd_ssh_SOURCES = \
|
|
|
6cf099 |
src/responder/ssh/sshsrv.c \
|
|
|
6cf099 |
src/responder/ssh/sshsrv_dp.c \
|
|
|
6cf099 |
src/responder/ssh/sshsrv_cmd.c \
|
|
|
6cf099 |
- $(SSSD_RESPONDER_OBJ)
|
|
|
6cf099 |
+ $(SSSD_RESPONDER_OBJ) \
|
|
|
6cf099 |
+ $(NULL)
|
|
|
6cf099 |
sssd_ssh_LDADD = \
|
|
|
6cf099 |
$(SSSD_LIBS) \
|
|
|
6cf099 |
- $(SSSD_INTERNAL_LTLIBS)
|
|
|
6cf099 |
+ $(SSSD_INTERNAL_LTLIBS) \
|
|
|
6cf099 |
+ libsss_cert.la \
|
|
|
6cf099 |
+ $(NULL)
|
|
|
6cf099 |
endif
|
|
|
6cf099 |
|
|
|
6cf099 |
sssd_pac_SOURCES = \
|
|
|
6cf099 |
diff --git a/src/confdb/confdb.h b/src/confdb/confdb.h
|
|
|
6cf099 |
index c3cdf49e08d10367e9e98c093a4aa2569b985170..df454337ab4d89c5857e73ee0e5392c2b4bba8b4 100644
|
|
|
6cf099 |
--- a/src/confdb/confdb.h
|
|
|
6cf099 |
+++ b/src/confdb/confdb.h
|
|
|
6cf099 |
@@ -135,6 +135,8 @@
|
|
|
6cf099 |
#define CONFDB_DEFAULT_SSH_HASH_KNOWN_HOSTS true
|
|
|
6cf099 |
#define CONFDB_SSH_KNOWN_HOSTS_TIMEOUT "ssh_known_hosts_timeout"
|
|
|
6cf099 |
#define CONFDB_DEFAULT_SSH_KNOWN_HOSTS_TIMEOUT 180
|
|
|
6cf099 |
+#define CONFDB_SSH_CA_DB "ca_db"
|
|
|
6cf099 |
+#define CONFDB_DEFAULT_SSH_CA_DB SYSCONFDIR"/pki/nssdb"
|
|
|
6cf099 |
|
|
|
6cf099 |
/* PAC */
|
|
|
6cf099 |
#define CONFDB_PAC_CONF_ENTRY "config/pac"
|
|
|
6cf099 |
diff --git a/src/config/SSSDConfig/__init__.py.in b/src/config/SSSDConfig/__init__.py.in
|
|
|
6cf099 |
index 4b519eddd04cde83c209f5a1940832cc7f41c736..7d361026c09ce8fd8d6a69f6bb3f3817bc3d68ba 100644
|
|
|
6cf099 |
--- a/src/config/SSSDConfig/__init__.py.in
|
|
|
6cf099 |
+++ b/src/config/SSSDConfig/__init__.py.in
|
|
|
6cf099 |
@@ -99,6 +99,7 @@ option_strings = {
|
|
|
6cf099 |
# [ssh]
|
|
|
6cf099 |
'ssh_hash_known_hosts': _('Whether to hash host names and addresses in the known_hosts file'),
|
|
|
6cf099 |
'ssh_known_hosts_timeout': _('How many seconds to keep a host in the known_hosts file after its host keys were requested'),
|
|
|
6cf099 |
+ 'ca_db': _('Path to storage of trusted CA certificates'),
|
|
|
6cf099 |
|
|
|
6cf099 |
# [pac]
|
|
|
6cf099 |
'allowed_uids': _('List of UIDs or user names allowed to access the PAC responder'),
|
|
|
6cf099 |
diff --git a/src/config/etc/sssd.api.conf b/src/config/etc/sssd.api.conf
|
|
|
6cf099 |
index 29fd896ccd7a3aa5ff81e15b771746a80ffc01af..cf6ce63012176d49f757afbc8a343b24aef869e8 100644
|
|
|
6cf099 |
--- a/src/config/etc/sssd.api.conf
|
|
|
6cf099 |
+++ b/src/config/etc/sssd.api.conf
|
|
|
6cf099 |
@@ -72,6 +72,7 @@ autofs_negative_timeout = int, None, false
|
|
|
6cf099 |
# ssh service
|
|
|
6cf099 |
ssh_hash_known_hosts = bool, None, false
|
|
|
6cf099 |
ssh_known_hosts_timeout = int, None, false
|
|
|
6cf099 |
+ca_db = str, None, false
|
|
|
6cf099 |
|
|
|
6cf099 |
[pac]
|
|
|
6cf099 |
# PAC responder
|
|
|
6cf099 |
diff --git a/src/man/sssd.conf.5.xml b/src/man/sssd.conf.5.xml
|
|
|
6cf099 |
index 7d3a57b0eadec48d64cc9ddcb226b89a1600b432..37e73515fbfcae0da492533de72ad3208c870e9b 100644
|
|
|
6cf099 |
--- a/src/man/sssd.conf.5.xml
|
|
|
6cf099 |
+++ b/src/man/sssd.conf.5.xml
|
|
|
6cf099 |
@@ -1082,6 +1082,19 @@ pam_account_expired_message = Account expired, please call help desk.
|
|
|
6cf099 |
</para>
|
|
|
6cf099 |
</listitem>
|
|
|
6cf099 |
</varlistentry>
|
|
|
6cf099 |
+ <varlistentry>
|
|
|
6cf099 |
+ <term>ca_db (string)</term>
|
|
|
6cf099 |
+ <listitem>
|
|
|
6cf099 |
+ <para>
|
|
|
6cf099 |
+ Path to a storage of trusted CA certificates. The
|
|
|
6cf099 |
+ option is used to validate user certificates before
|
|
|
6cf099 |
+ deriving public ssh keys from them.
|
|
|
6cf099 |
+ </para>
|
|
|
6cf099 |
+ <para>
|
|
|
6cf099 |
+ Default: /etc/pki/nssdb
|
|
|
6cf099 |
+ </para>
|
|
|
6cf099 |
+ </listitem>
|
|
|
6cf099 |
+ </varlistentry>
|
|
|
6cf099 |
</variablelist>
|
|
|
6cf099 |
</refsect2>
|
|
|
6cf099 |
|
|
|
6cf099 |
diff --git a/src/responder/ssh/sshsrv.c b/src/responder/ssh/sshsrv.c
|
|
|
6cf099 |
index 9439b9d89ae47dc66d392f0c434f4de1c1c0b4ea..d4e202d87f520f1bdcd521733592027773a821d6 100644
|
|
|
6cf099 |
--- a/src/responder/ssh/sshsrv.c
|
|
|
6cf099 |
+++ b/src/responder/ssh/sshsrv.c
|
|
|
6cf099 |
@@ -163,6 +163,15 @@ int ssh_process_init(TALLOC_CTX *mem_ctx,
|
|
|
6cf099 |
goto fail;
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
+ ret = confdb_get_string(ssh_ctx->rctx->cdb, ssh_ctx,
|
|
|
6cf099 |
+ CONFDB_SSH_CONF_ENTRY, CONFDB_SSH_CA_DB,
|
|
|
6cf099 |
+ CONFDB_DEFAULT_SSH_CA_DB, &ssh_ctx->ca_db);
|
|
|
6cf099 |
+ if (ret != EOK) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_FATAL_FAILURE, "Error reading CA DB from confdb (%d) [%s]\n",
|
|
|
6cf099 |
+ ret, strerror(ret));
|
|
|
6cf099 |
+ goto fail;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
ret = schedule_get_domains_task(rctx, rctx->ev, rctx, NULL);
|
|
|
6cf099 |
if (ret != EOK) {
|
|
|
6cf099 |
DEBUG(SSSDBG_FATAL_FAILURE, "schedule_get_domains_tasks failed.\n");
|
|
|
6cf099 |
diff --git a/src/responder/ssh/sshsrv_cmd.c b/src/responder/ssh/sshsrv_cmd.c
|
|
|
6cf099 |
index 4833587910cade32ecb0b5f65b417d58a498b01e..f630e5f0311dadc69bee59afb672720f7018169d 100644
|
|
|
6cf099 |
--- a/src/responder/ssh/sshsrv_cmd.c
|
|
|
6cf099 |
+++ b/src/responder/ssh/sshsrv_cmd.c
|
|
|
6cf099 |
@@ -27,6 +27,7 @@
|
|
|
6cf099 |
#include "util/util.h"
|
|
|
6cf099 |
#include "util/crypto/sss_crypto.h"
|
|
|
6cf099 |
#include "util/sss_ssh.h"
|
|
|
6cf099 |
+#include "util/cert.h"
|
|
|
6cf099 |
#include "db/sysdb.h"
|
|
|
6cf099 |
#include "db/sysdb_ssh.h"
|
|
|
6cf099 |
#include "providers/data_provider.h"
|
|
|
6cf099 |
@@ -219,7 +220,8 @@ static errno_t
|
|
|
6cf099 |
ssh_user_pubkeys_search_next(struct ssh_cmd_ctx *cmd_ctx)
|
|
|
6cf099 |
{
|
|
|
6cf099 |
errno_t ret;
|
|
|
6cf099 |
- const char *attrs[] = { SYSDB_NAME, SYSDB_SSH_PUBKEY, NULL };
|
|
|
6cf099 |
+ const char *attrs[] = { SYSDB_NAME, SYSDB_SSH_PUBKEY, SYSDB_USER_CERT,
|
|
|
6cf099 |
+ NULL };
|
|
|
6cf099 |
struct ldb_result *res;
|
|
|
6cf099 |
|
|
|
6cf099 |
DEBUG(SSSDBG_TRACE_FUNC,
|
|
|
6cf099 |
@@ -794,6 +796,8 @@ ssh_cmd_parse_request(struct ssh_cmd_ctx *cmd_ctx)
|
|
|
6cf099 |
|
|
|
6cf099 |
static errno_t decode_and_add_base64_data(struct ssh_cmd_ctx *cmd_ctx,
|
|
|
6cf099 |
struct ldb_message_element *el,
|
|
|
6cf099 |
+ bool cert_data,
|
|
|
6cf099 |
+ struct ssh_ctx *ssh_ctx,
|
|
|
6cf099 |
size_t fqname_len,
|
|
|
6cf099 |
const char *fqname,
|
|
|
6cf099 |
size_t *c)
|
|
|
6cf099 |
@@ -819,12 +823,22 @@ static errno_t decode_and_add_base64_data(struct ssh_cmd_ctx *cmd_ctx,
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
for (d = 0; d < el->num_values; d++) {
|
|
|
6cf099 |
- key = sss_base64_decode(tmp_ctx, (const char *) el->values[d].data,
|
|
|
6cf099 |
- &key_len);
|
|
|
6cf099 |
- if (key == NULL) {
|
|
|
6cf099 |
- DEBUG(SSSDBG_OP_FAILURE, "sss_base64_decode failed.\n");
|
|
|
6cf099 |
- ret = ENOMEM;
|
|
|
6cf099 |
- goto done;
|
|
|
6cf099 |
+ if (cert_data) {
|
|
|
6cf099 |
+ ret = cert_to_ssh_key(tmp_ctx, ssh_ctx->ca_db,
|
|
|
6cf099 |
+ el->values[d].data, el->values[d].length,
|
|
|
6cf099 |
+ &key, &key_len);
|
|
|
6cf099 |
+ if (ret != EOK) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "cert_to_ssh_key failed.\n");
|
|
|
6cf099 |
+ return ret;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+ } else {
|
|
|
6cf099 |
+ key = sss_base64_decode(tmp_ctx, (const char *) el->values[d].data,
|
|
|
6cf099 |
+ &key_len);
|
|
|
6cf099 |
+ if (key == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "sss_base64_decode failed.\n");
|
|
|
6cf099 |
+ ret = ENOMEM;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
ret = sss_packet_grow(cctx->creq->out,
|
|
|
6cf099 |
@@ -862,10 +876,13 @@ ssh_cmd_build_reply(struct ssh_cmd_ctx *cmd_ctx)
|
|
|
6cf099 |
struct ldb_message_element *el = NULL;
|
|
|
6cf099 |
struct ldb_message_element *el_override = NULL;
|
|
|
6cf099 |
struct ldb_message_element *el_orig = NULL;
|
|
|
6cf099 |
+ struct ldb_message_element *el_user_cert = NULL;
|
|
|
6cf099 |
uint32_t count = 0;
|
|
|
6cf099 |
const char *name;
|
|
|
6cf099 |
char *fqname;
|
|
|
6cf099 |
uint32_t fqname_len;
|
|
|
6cf099 |
+ struct ssh_ctx *ssh_ctx = talloc_get_type(cctx->rctx->pvt_ctx,
|
|
|
6cf099 |
+ struct ssh_ctx);
|
|
|
6cf099 |
|
|
|
6cf099 |
ret = sss_packet_new(cctx->creq, 0,
|
|
|
6cf099 |
sss_packet_get_cmd(cctx->creq->in),
|
|
|
6cf099 |
@@ -893,6 +910,12 @@ ssh_cmd_build_reply(struct ssh_cmd_ctx *cmd_ctx)
|
|
|
6cf099 |
}
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
+ el_user_cert = ldb_msg_find_element(cmd_ctx->result, SYSDB_USER_CERT);
|
|
|
6cf099 |
+ if (el_user_cert) {
|
|
|
6cf099 |
+ /* TODO check if cert is valid */
|
|
|
6cf099 |
+ count += el_user_cert->num_values;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
ret = sss_packet_grow(cctx->creq->out, 2*sizeof(uint32_t));
|
|
|
6cf099 |
if (ret != EOK) {
|
|
|
6cf099 |
return ret;
|
|
|
6cf099 |
@@ -922,20 +945,29 @@ ssh_cmd_build_reply(struct ssh_cmd_ctx *cmd_ctx)
|
|
|
6cf099 |
|
|
|
6cf099 |
fqname_len = strlen(fqname)+1;
|
|
|
6cf099 |
|
|
|
6cf099 |
- ret = decode_and_add_base64_data(cmd_ctx, el, fqname_len, fqname, &c);
|
|
|
6cf099 |
+ ret = decode_and_add_base64_data(cmd_ctx, el, false, ssh_ctx,
|
|
|
6cf099 |
+ fqname_len, fqname, &c);
|
|
|
6cf099 |
if (ret != EOK) {
|
|
|
6cf099 |
DEBUG(SSSDBG_OP_FAILURE, "decode_and_add_base64_data failed.\n");
|
|
|
6cf099 |
return ret;
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
- ret = decode_and_add_base64_data(cmd_ctx, el_orig, fqname_len, fqname, &c);
|
|
|
6cf099 |
+ ret = decode_and_add_base64_data(cmd_ctx, el_orig, false, ssh_ctx,
|
|
|
6cf099 |
+ fqname_len, fqname, &c);
|
|
|
6cf099 |
if (ret != EOK) {
|
|
|
6cf099 |
DEBUG(SSSDBG_OP_FAILURE, "decode_and_add_base64_data failed.\n");
|
|
|
6cf099 |
return ret;
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
- ret = decode_and_add_base64_data(cmd_ctx, el_override, fqname_len, fqname,
|
|
|
6cf099 |
- &c);
|
|
|
6cf099 |
+ ret = decode_and_add_base64_data(cmd_ctx, el_override, false, ssh_ctx,
|
|
|
6cf099 |
+ fqname_len, fqname, &c);
|
|
|
6cf099 |
+ if (ret != EOK) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "decode_and_add_base64_data failed.\n");
|
|
|
6cf099 |
+ return ret;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ ret = decode_and_add_base64_data(cmd_ctx, el_user_cert, true, ssh_ctx,
|
|
|
6cf099 |
+ fqname_len, fqname, &c);
|
|
|
6cf099 |
if (ret != EOK) {
|
|
|
6cf099 |
DEBUG(SSSDBG_OP_FAILURE, "decode_and_add_base64_data failed.\n");
|
|
|
6cf099 |
return ret;
|
|
|
6cf099 |
diff --git a/src/responder/ssh/sshsrv_private.h b/src/responder/ssh/sshsrv_private.h
|
|
|
6cf099 |
index ebb30ce7cbc982bb29b73592d5873e7d3652228a..beb8e18db77d8224e49df141748484ce61b11dac 100644
|
|
|
6cf099 |
--- a/src/responder/ssh/sshsrv_private.h
|
|
|
6cf099 |
+++ b/src/responder/ssh/sshsrv_private.h
|
|
|
6cf099 |
@@ -32,6 +32,7 @@ struct ssh_ctx {
|
|
|
6cf099 |
|
|
|
6cf099 |
bool hash_known_hosts;
|
|
|
6cf099 |
int known_hosts_timeout;
|
|
|
6cf099 |
+ char *ca_db;
|
|
|
6cf099 |
};
|
|
|
6cf099 |
|
|
|
6cf099 |
struct ssh_cmd_ctx {
|
|
|
6cf099 |
diff --git a/src/tests/cmocka/test_cert_utils.c b/src/tests/cmocka/test_cert_utils.c
|
|
|
6cf099 |
index 8063b1a65e8692142cbb3cf82fe41afa6567bc91..7bd8cf2344003421e9ec84dc5e1b2305a861ab38 100644
|
|
|
6cf099 |
--- a/src/tests/cmocka/test_cert_utils.c
|
|
|
6cf099 |
+++ b/src/tests/cmocka/test_cert_utils.c
|
|
|
6cf099 |
@@ -21,15 +21,18 @@
|
|
|
6cf099 |
You should have received a copy of the GNU General Public License
|
|
|
6cf099 |
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
6cf099 |
*/
|
|
|
6cf099 |
+#include "config.h"
|
|
|
6cf099 |
|
|
|
6cf099 |
#include <popt.h>
|
|
|
6cf099 |
#ifdef HAVE_LIBCRYPTO
|
|
|
6cf099 |
#include <openssl/objects.h>
|
|
|
6cf099 |
+#include <openssl/crypto.h>
|
|
|
6cf099 |
#endif
|
|
|
6cf099 |
|
|
|
6cf099 |
#include "util/cert.h"
|
|
|
6cf099 |
#include "tests/cmocka/common_mock.h"
|
|
|
6cf099 |
#include "util/crypto/nss/nss_util.h"
|
|
|
6cf099 |
+#include "util/crypto/sss_crypto.h"
|
|
|
6cf099 |
|
|
|
6cf099 |
|
|
|
6cf099 |
/* TODO: create a certificate for this test */
|
|
|
6cf099 |
@@ -306,6 +309,63 @@ void test_sss_cert_derb64_to_ldap_filter(void **state)
|
|
|
6cf099 |
talloc_free(filter);
|
|
|
6cf099 |
}
|
|
|
6cf099 |
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+#define SSH_TEST_CERT \
|
|
|
6cf099 |
+"MIIECTCCAvGgAwIBAgIBCDANBgkqhkiG9w0BAQsFADA0MRIwEAYDVQQKDAlJUEEu" \
|
|
|
6cf099 |
+"REVWRUwxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xNTA2MjMx" \
|
|
|
6cf099 |
+"NjMyMDdaFw0xNzA2MjMxNjMyMDdaMDIxEjAQBgNVBAoMCUlQQS5ERVZFTDEcMBoG" \
|
|
|
6cf099 |
+"A1UEAwwTaXBhLWRldmVsLmlwYS5kZXZlbDCCASIwDQYJKoZIhvcNAQEBBQADggEP" \
|
|
|
6cf099 |
+"ADCCAQoCggEBALXUq56VlY+Z0aWLLpFAjFfbElPBXGQsbZb85J3cGyPjaMHC9wS+" \
|
|
|
6cf099 |
+"wjB6Ve4HmQyPLx8hbINdDmbawMHYQvTScLYfsqLtj0Lqw20sUUmedk+Es5Oh9VHo" \
|
|
|
6cf099 |
+"nd8MavYx25Du2u+T0iSgNIDikXguiwCmtAj8VC49ebbgITcjJGzMmiiuJkV3o93Y" \
|
|
|
6cf099 |
+"vvYF0VjLGDQbQWOy7IxzYJeNVJnZWKo67CHdok6qOrm9rxQt81rzwV/mGLbCMUbr" \
|
|
|
6cf099 |
+"+N4M8URtd7EmzaYZQmNm//s2owFrCYMxpLiURPj+URZVuB72504/Ix7X0HCbA/AV" \
|
|
|
6cf099 |
+"26J27fPY5nc8DMwfhUDCbTqPH/JEjd3mvY8CAwEAAaOCASYwggEiMB8GA1UdIwQY" \
|
|
|
6cf099 |
+"MBaAFJOq+KAQmPEnNp8Wok23eGTdE7aDMDsGCCsGAQUFBwEBBC8wLTArBggrBgEF" \
|
|
|
6cf099 |
+"BQcwAYYfaHR0cDovL2lwYS1jYS5pcGEuZGV2ZWwvY2Evb2NzcDAOBgNVHQ8BAf8E" \
|
|
|
6cf099 |
+"BAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHQGA1UdHwRtMGsw" \
|
|
|
6cf099 |
+"aaAxoC+GLWh0dHA6Ly9pcGEtY2EuaXBhLmRldmVsL2lwYS9jcmwvTWFzdGVyQ1JM" \
|
|
|
6cf099 |
+"LmJpbqI0pDIwMDEOMAwGA1UECgwFaXBhY2ExHjAcBgNVBAMMFUNlcnRpZmljYXRl" \
|
|
|
6cf099 |
+"IEF1dGhvcml0eTAdBgNVHQ4EFgQUFaDNd5a53QGpaw5m63hnwXicMQ8wDQYJKoZI" \
|
|
|
6cf099 |
+"hvcNAQELBQADggEBADH7Nj00qqGhGJeXJQAsepqSskz/wooqXh8vgVyb8SS4N0/c" \
|
|
|
6cf099 |
+"0aQtVmY81xamlXE12ZFpwDX43d+EufBkwCUKFX/+8JFDd2doAyeJxv1xM22kKRpc" \
|
|
|
6cf099 |
+"AqITPgMsa9ToGMWxjbVpc/X/5YfZixWPF0/eZUTotBj9oaR039UrhGfyN7OguF/G" \
|
|
|
6cf099 |
+"rzmxtB5y4ZrMpcD/Oe90mkd9HY7sA/fB8OWOUgeRfQoh97HNS0UiDWsPtfxmjQG5" \
|
|
|
6cf099 |
+"zotpoBIZmdH+ipYsu58HohHVlM9Wi5H4QmiiXl+Soldkq7eXYlafcmT7wv8+cKwz" \
|
|
|
6cf099 |
+"Nz0Tm3+eYpFqRo3skr6QzXi525Jkg3r6r+kkhxU="
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+#define SSH_PUB_KEY "AAAAB3NzaC1yc2EAAAADAQABAAABAQC11KuelZWPmdGliy6RQIxX2xJTwVxkLG2W/OSd3Bsj42jBwvcEvsIwelXuB5kMjy8fIWyDXQ5m2sDB2EL00nC2H7Ki7Y9C6sNtLFFJnnZPhLOTofVR6J3fDGr2MduQ7trvk9IkoDSA4pF4LosAprQI/FQuPXm24CE3IyRszJooriZFd6Pd2L72BdFYyxg0G0FjsuyMc2CXjVSZ2ViqOuwh3aJOqjq5va8ULfNa88Ff5hi2wjFG6/jeDPFEbXexJs2mGUJjZv/7NqMBawmDMaS4lET4/lEWVbge9udOPyMe19BwmwPwFduidu3z2OZ3PAzMH4VAwm06jx/yRI3d5r2P"
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+void test_cert_to_ssh_key(void **state)
|
|
|
6cf099 |
+{
|
|
|
6cf099 |
+ int ret;
|
|
|
6cf099 |
+ uint8_t *key;
|
|
|
6cf099 |
+ size_t key_size;
|
|
|
6cf099 |
+ uint8_t *exp_key;
|
|
|
6cf099 |
+ size_t exp_key_size;
|
|
|
6cf099 |
+ uint8_t *der;
|
|
|
6cf099 |
+ size_t der_size;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ struct test_state *ts = talloc_get_type_abort(*state, struct test_state);
|
|
|
6cf099 |
+ assert_non_null(ts);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ der = sss_base64_decode(ts, SSH_TEST_CERT, &der_size);
|
|
|
6cf099 |
+ assert_non_null(der);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ exp_key = sss_base64_decode(ts, SSH_PUB_KEY, &exp_key_size);
|
|
|
6cf099 |
+ assert_non_null(exp_key);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ ret = cert_to_ssh_key(ts, "sql:" ABS_SRC_DIR "/src/tests/cmocka/p11_nssdb",
|
|
|
6cf099 |
+ der, der_size, &key, &key_size);
|
|
|
6cf099 |
+ assert_int_equal(ret, EOK);
|
|
|
6cf099 |
+ assert_int_equal(key_size, exp_key_size);
|
|
|
6cf099 |
+ assert_memory_equal(key, exp_key, exp_key_size);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ talloc_free(der);
|
|
|
6cf099 |
+ talloc_free(key);
|
|
|
6cf099 |
+ talloc_free(exp_key);
|
|
|
6cf099 |
+}
|
|
|
6cf099 |
+
|
|
|
6cf099 |
int main(int argc, const char *argv[])
|
|
|
6cf099 |
{
|
|
|
6cf099 |
poptContext pc;
|
|
|
6cf099 |
@@ -330,6 +390,8 @@ int main(int argc, const char *argv[])
|
|
|
6cf099 |
setup, teardown),
|
|
|
6cf099 |
cmocka_unit_test_setup_teardown(test_sss_cert_derb64_to_ldap_filter,
|
|
|
6cf099 |
setup, teardown),
|
|
|
6cf099 |
+ cmocka_unit_test_setup_teardown(test_cert_to_ssh_key,
|
|
|
6cf099 |
+ setup, teardown),
|
|
|
6cf099 |
};
|
|
|
6cf099 |
|
|
|
6cf099 |
/* Set debug level to invalid value so we can deside if -d 0 was used. */
|
|
|
6cf099 |
diff --git a/src/util/cert.h b/src/util/cert.h
|
|
|
6cf099 |
index 79ea1a4ab58149a312bece265798ab3a3f459114..edbafc492a1ed42ad616d0bf2fae882046711746 100644
|
|
|
6cf099 |
--- a/src/util/cert.h
|
|
|
6cf099 |
+++ b/src/util/cert.h
|
|
|
6cf099 |
@@ -44,4 +44,8 @@ errno_t sss_cert_derb64_to_ldap_filter(TALLOC_CTX *mem_ctx, const char *derb64,
|
|
|
6cf099 |
errno_t bin_to_ldap_filter_value(TALLOC_CTX *mem_ctx,
|
|
|
6cf099 |
const uint8_t *blob, size_t blob_size,
|
|
|
6cf099 |
char **_str);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+errno_t cert_to_ssh_key(TALLOC_CTX *mem_ctx, const char *ca_db,
|
|
|
6cf099 |
+ const uint8_t *der_blob, size_t der_size,
|
|
|
6cf099 |
+ uint8_t **key, size_t *key_size);
|
|
|
6cf099 |
#endif /* __CERT_H__ */
|
|
|
6cf099 |
diff --git a/src/util/cert/libcrypto/cert.c b/src/util/cert/libcrypto/cert.c
|
|
|
6cf099 |
index 1a250f60d1a7dfd5c883ec7e9b1f9491fb74c9b0..01f9554b990d6a139bb9a1d8d558c1c3f6bb745c 100644
|
|
|
6cf099 |
--- a/src/util/cert/libcrypto/cert.c
|
|
|
6cf099 |
+++ b/src/util/cert/libcrypto/cert.c
|
|
|
6cf099 |
@@ -166,3 +166,96 @@ done:
|
|
|
6cf099 |
return ret;
|
|
|
6cf099 |
|
|
|
6cf099 |
}
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+#define SSH_RSA_HEADER "ssh-rsa"
|
|
|
6cf099 |
+#define SSH_RSA_HEADER_LEN (sizeof(SSH_RSA_HEADER) - 1)
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+errno_t cert_to_ssh_key(TALLOC_CTX *mem_ctx, const char *ca_db,
|
|
|
6cf099 |
+ const uint8_t *der_blob, size_t der_size,
|
|
|
6cf099 |
+ uint8_t **key, size_t *key_size)
|
|
|
6cf099 |
+{
|
|
|
6cf099 |
+ int ret;
|
|
|
6cf099 |
+ size_t size;
|
|
|
6cf099 |
+ const unsigned char *d;
|
|
|
6cf099 |
+ uint8_t *buf = NULL;
|
|
|
6cf099 |
+ size_t c;
|
|
|
6cf099 |
+ X509 *cert = NULL;
|
|
|
6cf099 |
+ EVP_PKEY *cert_pub_key = NULL;
|
|
|
6cf099 |
+ int modulus_len;
|
|
|
6cf099 |
+ unsigned char modulus[OPENSSL_RSA_MAX_MODULUS_BITS/8];
|
|
|
6cf099 |
+ int exponent_len;
|
|
|
6cf099 |
+ unsigned char exponent[OPENSSL_RSA_MAX_PUBEXP_BITS/8];
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ if (der_blob == NULL || der_size == 0) {
|
|
|
6cf099 |
+ return EINVAL;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ d = (const unsigned char *) der_blob;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ cert = d2i_X509(NULL, &d, (int) der_size);
|
|
|
6cf099 |
+ if (cert == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "d2i_X509 failed.\n");
|
|
|
6cf099 |
+ return EINVAL;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ /* TODO: verify certificate !!!!! */
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ cert_pub_key = X509_get_pubkey(cert);
|
|
|
6cf099 |
+ if (cert_pub_key == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "X509_get_pubkey failed.\n");
|
|
|
6cf099 |
+ ret = EIO;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ if (cert_pub_key->type != EVP_PKEY_RSA) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_CRIT_FAILURE,
|
|
|
6cf099 |
+ "Expected RSA public key, found unsupported [%d].\n",
|
|
|
6cf099 |
+ cert_pub_key->type);
|
|
|
6cf099 |
+ ret = EINVAL;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ modulus_len = BN_bn2bin(cert_pub_key->pkey.rsa->n, modulus);
|
|
|
6cf099 |
+ exponent_len = BN_bn2bin(cert_pub_key->pkey.rsa->e, exponent);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ size = SSH_RSA_HEADER_LEN + 3 * sizeof(uint32_t)
|
|
|
6cf099 |
+ + modulus_len
|
|
|
6cf099 |
+ + exponent_len
|
|
|
6cf099 |
+ + 1; /* see comment about missing 00 below */
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ buf = talloc_size(mem_ctx, size);
|
|
|
6cf099 |
+ if (buf == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n");
|
|
|
6cf099 |
+ ret = ENOMEM;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ c = 0;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ SAFEALIGN_SET_UINT32(buf, htobe32(SSH_RSA_HEADER_LEN), &c);
|
|
|
6cf099 |
+ safealign_memcpy(&buf[c], SSH_RSA_HEADER, SSH_RSA_HEADER_LEN, &c);
|
|
|
6cf099 |
+ SAFEALIGN_SET_UINT32(&buf[c], htobe32(exponent_len), &c);
|
|
|
6cf099 |
+ safealign_memcpy(&buf[c], exponent, exponent_len, &c);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ /* Adding missing 00 which afaik is added to make sure
|
|
|
6cf099 |
+ * the bigint is handled as positive number */
|
|
|
6cf099 |
+ /* TODO: make a better check if 00 must be added or not, e.g. ... & 0x80)
|
|
|
6cf099 |
+ */
|
|
|
6cf099 |
+ SAFEALIGN_SET_UINT32(&buf[c], htobe32(modulus_len + 1), &c);
|
|
|
6cf099 |
+ SAFEALIGN_SETMEM_VALUE(&buf[c], '\0', unsigned char, &c);
|
|
|
6cf099 |
+ safealign_memcpy(&buf[c], modulus, modulus_len, &c);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ *key = buf;
|
|
|
6cf099 |
+ *key_size = size;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ ret = EOK;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+done:
|
|
|
6cf099 |
+ if (ret != EOK) {
|
|
|
6cf099 |
+ talloc_free(buf);
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+ EVP_PKEY_free(cert_pub_key);
|
|
|
6cf099 |
+ X509_free(cert);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ return ret;
|
|
|
6cf099 |
+}
|
|
|
6cf099 |
diff --git a/src/util/cert/nss/cert.c b/src/util/cert/nss/cert.c
|
|
|
6cf099 |
index a20abf63a10de9a5e9810f1c7d56686d063d60c7..1ada35b6321b9e0f3daf87b3412cf7a124cbf2c7 100644
|
|
|
6cf099 |
--- a/src/util/cert/nss/cert.c
|
|
|
6cf099 |
+++ b/src/util/cert/nss/cert.c
|
|
|
6cf099 |
@@ -20,9 +20,13 @@
|
|
|
6cf099 |
|
|
|
6cf099 |
#include "util/util.h"
|
|
|
6cf099 |
|
|
|
6cf099 |
+#include <nss.h>
|
|
|
6cf099 |
#include <cert.h>
|
|
|
6cf099 |
#include <base64.h>
|
|
|
6cf099 |
+#include <key.h>
|
|
|
6cf099 |
+#include <prerror.h>
|
|
|
6cf099 |
|
|
|
6cf099 |
+#include "util/crypto/sss_crypto.h"
|
|
|
6cf099 |
#include "util/crypto/nss/nss_util.h"
|
|
|
6cf099 |
|
|
|
6cf099 |
#define NS_CERT_HEADER "-----BEGIN CERTIFICATE-----"
|
|
|
6cf099 |
@@ -210,3 +214,129 @@ done:
|
|
|
6cf099 |
|
|
|
6cf099 |
return ret;
|
|
|
6cf099 |
}
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+#define SSH_RSA_HEADER "ssh-rsa"
|
|
|
6cf099 |
+#define SSH_RSA_HEADER_LEN (sizeof(SSH_RSA_HEADER) - 1)
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+errno_t cert_to_ssh_key(TALLOC_CTX *mem_ctx, const char *ca_db,
|
|
|
6cf099 |
+ const uint8_t *der_blob, size_t der_size,
|
|
|
6cf099 |
+ uint8_t **key, size_t *key_size)
|
|
|
6cf099 |
+{
|
|
|
6cf099 |
+ CERTCertDBHandle *handle;
|
|
|
6cf099 |
+ CERTCertificate *cert = NULL;
|
|
|
6cf099 |
+ SECItem der_item;
|
|
|
6cf099 |
+ SECKEYPublicKey *cert_pub_key = NULL;
|
|
|
6cf099 |
+ int ret;
|
|
|
6cf099 |
+ size_t size;
|
|
|
6cf099 |
+ uint8_t *buf = NULL;
|
|
|
6cf099 |
+ size_t c;
|
|
|
6cf099 |
+ NSSInitContext *nss_ctx;
|
|
|
6cf099 |
+ NSSInitParameters parameters = { 0 };
|
|
|
6cf099 |
+ parameters.length = sizeof (parameters);
|
|
|
6cf099 |
+ SECStatus rv;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ if (der_blob == NULL || der_size == 0) {
|
|
|
6cf099 |
+ return EINVAL;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ /* initialize NSS with context, we might have already called
|
|
|
6cf099 |
+ * NSS_NoDB_Init() but for validation we need to have access to a DB with
|
|
|
6cf099 |
+ * the trusted issuer cert. Only NSS_InitContext will really open the DB
|
|
|
6cf099 |
+ * in this case. I'm not sure about how long validation might need e.g. if
|
|
|
6cf099 |
+ * CRLs or OSCP is enabled, maybe it would be better to run validation in
|
|
|
6cf099 |
+ * p11_child ? */
|
|
|
6cf099 |
+ nss_ctx = NSS_InitContext(ca_db, "", "", SECMOD_DB, ¶meters,
|
|
|
6cf099 |
+ NSS_INIT_READONLY);
|
|
|
6cf099 |
+ if (nss_ctx == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "NSS_InitContext failed [%d].\n",
|
|
|
6cf099 |
+ PR_GetError());
|
|
|
6cf099 |
+ return EIO;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ handle = CERT_GetDefaultCertDB();
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ der_item.len = der_size;
|
|
|
6cf099 |
+ der_item.data = discard_const(der_blob);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ cert = CERT_NewTempCertificate(handle, &der_item, NULL, PR_FALSE, PR_TRUE);
|
|
|
6cf099 |
+ if (cert == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "CERT_NewTempCertificate failed.\n");
|
|
|
6cf099 |
+ ret = EINVAL;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ rv = CERT_VerifyCertificateNow(handle, cert, PR_TRUE,
|
|
|
6cf099 |
+ certificateUsageSSLClient, NULL, NULL);
|
|
|
6cf099 |
+ if (rv != SECSuccess) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_CRIT_FAILURE, "CERT_VerifyCertificateNow failed [%d].\n",
|
|
|
6cf099 |
+ PR_GetError());
|
|
|
6cf099 |
+ ret = EACCES;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ cert_pub_key = CERT_ExtractPublicKey(cert);
|
|
|
6cf099 |
+ if (cert_pub_key == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "CERT_ExtractPublicKey failed.\n");
|
|
|
6cf099 |
+ ret = EIO;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ if (cert_pub_key->keyType != rsaKey) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_CRIT_FAILURE,
|
|
|
6cf099 |
+ "Expected RSA public key, found unsupported [%d].\n",
|
|
|
6cf099 |
+ cert_pub_key->keyType);
|
|
|
6cf099 |
+ ret = EINVAL;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ size = SSH_RSA_HEADER_LEN + 3 * sizeof(uint32_t)
|
|
|
6cf099 |
+ + cert_pub_key->u.rsa.modulus.len
|
|
|
6cf099 |
+ + cert_pub_key->u.rsa.publicExponent.len
|
|
|
6cf099 |
+ + 1; /* see comment about missing 00 below */
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ buf = talloc_size(mem_ctx, size);
|
|
|
6cf099 |
+ if (buf == NULL) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "talloc_size failed.\n");
|
|
|
6cf099 |
+ ret = ENOMEM;
|
|
|
6cf099 |
+ goto done;
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ c = 0;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ SAFEALIGN_SET_UINT32(buf, htobe32(SSH_RSA_HEADER_LEN), &c);
|
|
|
6cf099 |
+ safealign_memcpy(&buf[c], SSH_RSA_HEADER, SSH_RSA_HEADER_LEN, &c);
|
|
|
6cf099 |
+ SAFEALIGN_SET_UINT32(&buf[c],
|
|
|
6cf099 |
+ htobe32(cert_pub_key->u.rsa.publicExponent.len), &c);
|
|
|
6cf099 |
+ safealign_memcpy(&buf[c], cert_pub_key->u.rsa.publicExponent.data,
|
|
|
6cf099 |
+ cert_pub_key->u.rsa.publicExponent.len, &c);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ /* Looks like nss drops the leading 00 which afaik is added to make sure
|
|
|
6cf099 |
+ * the bigint is handled as positive number */
|
|
|
6cf099 |
+ /* TODO: make a better check if 00 must be added or not, e.g. ... & 0x80)
|
|
|
6cf099 |
+ */
|
|
|
6cf099 |
+ SAFEALIGN_SET_UINT32(&buf[c],
|
|
|
6cf099 |
+ htobe32(cert_pub_key->u.rsa.modulus.len + 1 ), &c);
|
|
|
6cf099 |
+ SAFEALIGN_SETMEM_VALUE(&buf[c], '\0', unsigned char, &c);
|
|
|
6cf099 |
+ safealign_memcpy(&buf[c], cert_pub_key->u.rsa.modulus.data,
|
|
|
6cf099 |
+ cert_pub_key->u.rsa.modulus.len, &c);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ *key = buf;
|
|
|
6cf099 |
+ *key_size = size;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ ret = EOK;
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+done:
|
|
|
6cf099 |
+ if (ret != EOK) {
|
|
|
6cf099 |
+ talloc_free(buf);
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+ SECKEY_DestroyPublicKey(cert_pub_key);
|
|
|
6cf099 |
+ CERT_DestroyCertificate(cert);
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ rv = NSS_ShutdownContext(nss_ctx);
|
|
|
6cf099 |
+ if (rv != SECSuccess) {
|
|
|
6cf099 |
+ DEBUG(SSSDBG_OP_FAILURE, "NSS_ShutdownContext failed [%d].\n",
|
|
|
6cf099 |
+ PR_GetError());
|
|
|
6cf099 |
+ }
|
|
|
6cf099 |
+
|
|
|
6cf099 |
+ return ret;
|
|
|
6cf099 |
+}
|
|
|
6cf099 |
--
|
|
|
6cf099 |
2.4.3
|
|
|
6cf099 |
|