From 9c6b7f2d62e134a4bc15efc04e05be25e4a53dc7 Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Thu, 1 Dec 2005 05:20:39 +0000 Subject: r11995: A big kerberos-related update. This merges Samba4 up to current lorikeet-heimdal, which includes a replacement for some Samba-specific hacks. In particular, the credentials system now supplies GSS client and server credentials. These are imported into GSS with gss_krb5_import_creds(). Unfortunetly this can't take an MEMORY keytab, so we now create a FILE based keytab as provision and join time. Because the keytab is now created in advance, we don't spend .4s at negprot doing sha1 s2k calls. Also, because the keytab is read in real time, any change in the server key will be correctly picked up by the the krb5 code. To mark entries in the secrets which should be exported to a keytab, there is a new kerberosSecret objectClass. The new routine cli_credentials_update_all_keytabs() searches for these, and updates the keytabs. This is called in the provision.js via the ejs wrapper credentials_update_all_keytabs(). We can now (in theory) use a system-provided /etc/krb5.keytab, if krb5Keytab: FILE:/etc/krb5.keytab is added to the secrets.ldb record. By default the attribute privateKeytab: secrets.keytab is set, pointing to allow the whole private directory to be moved without breaking the internal links. (This used to be commit 6b75573df49c6210e1b9d71e108a9490976bd41d) --- source4/auth/credentials/credentials.c | 27 +++- source4/auth/credentials/credentials.h | 6 +- source4/auth/credentials/credentials_files.c | 92 +++++++++++- source4/auth/credentials/credentials_krb5.c | 211 +++++++++++++++++++++++++-- source4/auth/gensec/gensec_gssapi.c | 61 +------- 5 files changed, 321 insertions(+), 76 deletions(-) (limited to 'source4/auth') diff --git a/source4/auth/credentials/credentials.c b/source4/auth/credentials/credentials.c index 86a3df0077..75c6795e73 100644 --- a/source4/auth/credentials/credentials.c +++ b/source4/auth/credentials/credentials.c @@ -46,7 +46,8 @@ struct cli_credentials *cli_credentials_init(TALLOC_CTX *mem_ctx) cred->domain_obtained = CRED_UNINITIALISED; cred->realm_obtained = CRED_UNINITIALISED; cred->ccache_obtained = CRED_UNINITIALISED; - cred->gss_creds_obtained = CRED_UNINITIALISED; + cred->client_gss_creds_obtained = CRED_UNINITIALISED; + cred->server_gss_creds_obtained = CRED_UNINITIALISED; cred->keytab_obtained = CRED_UNINITIALISED; cred->principal_obtained = CRED_UNINITIALISED; @@ -148,6 +149,9 @@ BOOL cli_credentials_set_principal(struct cli_credentials *cred, return False; } +/* Set a callback to get the principal. This could be a popup dialog, + * a terminal prompt or similar. */ + BOOL cli_credentials_set_principal_callback(struct cli_credentials *cred, const char *(*principal_cb) (struct cli_credentials *)) { @@ -160,6 +164,10 @@ BOOL cli_credentials_set_principal_callback(struct cli_credentials *cred, return False; } +/* Some of our tools are 'anonymous by default'. This is a single + * function to determine if authentication has been explicitly + * requested */ + BOOL cli_credentials_authentication_requested(struct cli_credentials *cred) { if (cred->principal_obtained >= CRED_SPECIFIED) { @@ -190,6 +198,9 @@ const char *cli_credentials_get_password(struct cli_credentials *cred) return cred->password; } +/* Set a password on the credentials context, including an indication + * of 'how' the password was obtained */ + BOOL cli_credentials_set_password(struct cli_credentials *cred, const char *val, enum credentials_obtained obtained) @@ -240,7 +251,11 @@ BOOL cli_credentials_set_old_password(struct cli_credentials *cred, } /** - * Obtain the password for this credentials context. + * Obtain the password, in the form MD4(unicode(password)) for this credentials context. + * + * Sometimes we only have this much of the password, while the rest of + * the time this call avoids calling E_md4hash themselves. + * * @param cred credentials context * @retval If set, the cleartext password, otherwise NULL */ @@ -566,7 +581,13 @@ void cli_credentials_set_anonymous(struct cli_credentials *cred) BOOL cli_credentials_is_anonymous(struct cli_credentials *cred) { - const char *username = cli_credentials_get_username(cred); + const char *username; + + if (cred->machine_account_pending) { + cli_credentials_set_machine_account(cred); + } + + username = cli_credentials_get_username(cred); /* Yes, it is deliberate that we die if we have a NULL pointer * here - anonymous is "", not NULL, which is 'never specified, diff --git a/source4/auth/credentials/credentials.h b/source4/auth/credentials/credentials.h index 3e84db52a5..81773aa70a 100644 --- a/source4/auth/credentials/credentials.h +++ b/source4/auth/credentials/credentials.h @@ -47,9 +47,10 @@ struct cli_credentials { enum credentials_obtained domain_obtained; enum credentials_obtained realm_obtained; enum credentials_obtained ccache_obtained; - enum credentials_obtained gss_creds_obtained; + enum credentials_obtained client_gss_creds_obtained; enum credentials_obtained principal_obtained; enum credentials_obtained keytab_obtained; + enum credentials_obtained server_gss_creds_obtained; const char *workstation; const char *username; @@ -63,8 +64,9 @@ struct cli_credentials { struct samr_Password *nt_hash; struct ccache_container *ccache; - struct gssapi_creds_container *gssapi_creds; + struct gssapi_creds_container *client_gss_creds; struct keytab_container *keytab; + struct gssapi_creds_container *server_gss_creds; const char *(*workstation_cb) (struct cli_credentials *); const char *(*password_cb) (struct cli_credentials *); diff --git a/source4/auth/credentials/credentials_files.c b/source4/auth/credentials/credentials_files.c index 35bbc43b34..6b3c77c4e3 100644 --- a/source4/auth/credentials/credentials_files.c +++ b/source4/auth/credentials/credentials_files.c @@ -164,9 +164,9 @@ BOOL cli_credentials_parse_file(struct cli_credentials *cred, const char *file, * @param cred Credentials structure to fill in * @retval NTSTATUS error detailing any failure */ -static NTSTATUS cli_credentials_set_secrets(struct cli_credentials *cred, - const char *base, - const char *filter) +NTSTATUS cli_credentials_set_secrets(struct cli_credentials *cred, + const char *base, + const char *filter) { TALLOC_CTX *mem_ctx; @@ -183,6 +183,8 @@ static NTSTATUS cli_credentials_set_secrets(struct cli_credentials *cred, "ntPwdHash", "msDS-KeyVersionNumber", "saltPrincipal", + "privateKeytab", + "krb5Keytab", NULL }; @@ -193,6 +195,7 @@ static NTSTATUS cli_credentials_set_secrets(struct cli_credentials *cred, const char *realm; enum netr_SchannelType sct; const char *salt_principal; + const char *keytab; /* ok, we are going to get it now, don't recurse back here */ cred->machine_account_pending = False; @@ -201,6 +204,7 @@ static NTSTATUS cli_credentials_set_secrets(struct cli_credentials *cred, cred->machine_account = True; mem_ctx = talloc_named(cred, 0, "cli_credentials fetch machine password"); + /* Local secrets are stored in secrets.ldb */ ldb = secrets_db_connect(mem_ctx); if (!ldb) { @@ -279,7 +283,22 @@ static NTSTATUS cli_credentials_set_secrets(struct cli_credentials *cred, } cli_credentials_set_kvno(cred, ldb_msg_find_int(msgs[0], "msDS-KeyVersionNumber", 0)); - + + /* If there was an external keytab specified by reference in + * the LDB, then use this. Otherwise we will make one up + * (chewing CPU time) from the password */ + keytab = ldb_msg_find_string(msgs[0], "krb5Keytab", NULL); + if (keytab) { + cli_credentials_set_keytab(cred, keytab, CRED_SPECIFIED); + } else { + keytab = ldb_msg_find_string(msgs[0], "privateKeytab", NULL); + if (keytab) { + keytab = talloc_asprintf(mem_ctx, "FILE:%s", private_path(mem_ctx, keytab)); + if (keytab) { + cli_credentials_set_keytab(cred, keytab, CRED_SPECIFIED); + } + } + } talloc_free(mem_ctx); return NT_STATUS_OK; @@ -345,3 +364,68 @@ void cli_credentials_set_machine_account_pending(struct cli_credentials *cred) cred->machine_account_pending = True; } + +NTSTATUS cli_credentials_update_all_keytabs(TALLOC_CTX *parent_ctx) +{ + TALLOC_CTX *mem_ctx; + int ldb_ret; + struct ldb_context *ldb; + struct ldb_message **msgs; + const char *attrs[] = { NULL }; + struct cli_credentials *creds; + const char *filter; + NTSTATUS status; + int i, ret; + + mem_ctx = talloc_new(parent_ctx); + if (!mem_ctx) { + return NT_STATUS_NO_MEMORY; + } + + /* Local secrets are stored in secrets.ldb */ + ldb = secrets_db_connect(mem_ctx); + if (!ldb) { + DEBUG(1, ("Could not open secrets.ldb\n")); + talloc_free(mem_ctx); + return NT_STATUS_ACCESS_DENIED; + } + + /* search for the secret record */ + ldb_ret = gendb_search(ldb, + mem_ctx, NULL, + &msgs, attrs, + "objectClass=kerberosSecret"); + if (ldb_ret == -1) { + DEBUG(1, ("Error looking for kerberos type secrets to push into a keytab")); + talloc_free(mem_ctx); + return NT_STATUS_INTERNAL_DB_CORRUPTION; + } + + for (i=0; i < ldb_ret; i++) { + /* Make a credentials structure from it */ + creds = cli_credentials_init(mem_ctx); + if (!creds) { + DEBUG(1, ("cli_credentials_init failed!")); + talloc_free(mem_ctx); + return NT_STATUS_NO_MEMORY; + } + cli_credentials_set_conf(creds); + filter = talloc_asprintf(mem_ctx, "dn=%s", ldb_dn_linearize(mem_ctx, msgs[i]->dn)); + status = cli_credentials_set_secrets(creds, NULL, filter); + if (!NT_STATUS_IS_OK(status)) { + DEBUG(1, ("Failed to read secrets for keytab update for %s\n", + filter)); + talloc_free(mem_ctx); + return status; + } + ret = cli_credentials_update_keytab(creds); + if (ret != 0) { + DEBUG(1, ("Failed to update keytab for %s\n", + filter)); + talloc_free(mem_ctx); + return NT_STATUS_UNSUCCESSFUL; + } + } + return NT_STATUS_OK; +} + diff --git a/source4/auth/credentials/credentials_krb5.c b/source4/auth/credentials/credentials_krb5.c index a3761e8359..173739e9b8 100644 --- a/source4/auth/credentials/credentials_krb5.c +++ b/source4/auth/credentials/credentials_krb5.c @@ -52,6 +52,10 @@ int cli_credentials_set_from_ccache(struct cli_credentials *cred, char *name; char **realm; + if (cred->ccache_obtained > obtained) { + return 0; + } + ret = krb5_cc_get_principal(cred->ccache->smb_krb5_context->krb5_context, cred->ccache->ccache, &princ); @@ -107,7 +111,12 @@ int cli_credentials_set_ccache(struct cli_credentials *cred, { krb5_error_code ret; krb5_principal princ; - struct ccache_container *ccc = talloc(cred, struct ccache_container); + struct ccache_container *ccc; + if (cred->ccache_obtained > obtained) { + return 0; + } + + ccc = talloc(cred, struct ccache_container); if (!ccc) { return ENOMEM; } @@ -265,10 +274,10 @@ int cli_credentials_get_client_gss_creds(struct cli_credentials *cred, OM_uint32 maj_stat, min_stat; struct gssapi_creds_container *gcc; struct ccache_container *ccache; - if (cred->gss_creds_obtained >= (MAX(cred->ccache_obtained, + if (cred->client_gss_creds_obtained >= (MAX(cred->ccache_obtained, MAX(cred->principal_obtained, cred->username_obtained)))) { - *_gcc = cred->gssapi_creds; + *_gcc = cred->client_gss_creds; return 0; } ret = cli_credentials_get_ccache(cred, @@ -283,8 +292,8 @@ int cli_credentials_get_client_gss_creds(struct cli_credentials *cred, return ENOMEM; } - maj_stat = gss_krb5_import_ccache(&min_stat, ccache->ccache, - &gcc->creds); + maj_stat = gss_krb5_import_cred(&min_stat, ccache->ccache, NULL, NULL, + &gcc->creds); if (maj_stat) { if (min_stat) { ret = min_stat; @@ -293,20 +302,20 @@ int cli_credentials_get_client_gss_creds(struct cli_credentials *cred, } } if (ret == 0) { - cred->gss_creds_obtained = cred->ccache_obtained; + cred->client_gss_creds_obtained = cred->ccache_obtained; talloc_set_destructor(gcc, free_gssapi_creds); - cred->gssapi_creds = gcc; + cred->client_gss_creds = gcc; *_gcc = gcc; } return ret; } /** - Set a gssapi cred_id_t into the credentails system. + Set a gssapi cred_id_t into the credentails system. (Client case) This grabs the credentials both 'intact' and getting the krb5 ccache out of it. This routine can be generalised in future for - the case where we deal with GSSAPI mechs other than krb5. + the case where we deal with GSSAPI mechs other than krb5. On sucess, the caller must not free gssapi_cred, as it now belongs to the credentials system. @@ -319,7 +328,12 @@ int cli_credentials_get_client_gss_creds(struct cli_credentials *cred, int ret; OM_uint32 maj_stat, min_stat; struct ccache_container *ccc; - struct gssapi_creds_container *gcc = talloc(cred, struct gssapi_creds_container); + struct gssapi_creds_container *gcc; + if (cred->client_gss_creds_obtained > obtained) { + return 0; + } + + gcc = talloc(cred, struct gssapi_creds_container); if (!gcc) { return ENOMEM; } @@ -346,18 +360,23 @@ int cli_credentials_get_client_gss_creds(struct cli_credentials *cred, gcc->creds = gssapi_cred; talloc_set_destructor(gcc, free_gssapi_creds); - cred->gss_creds_obtained = obtained; - cred->gssapi_creds = gcc; + cred->client_gss_creds_obtained = obtained; + cred->client_gss_creds = gcc; } return ret; } +/* Get the keytab (actually, a container containing the krb5_keytab) + * attached to this context. If this hasn't been done or set before, + * it will be generated from the password. + */ int cli_credentials_get_keytab(struct cli_credentials *cred, struct keytab_container **_ktc) { krb5_error_code ret; struct keytab_container *ktc; struct smb_krb5_context *smb_krb5_context; + TALLOC_CTX *mem_ctx; if (cred->keytab_obtained >= (MAX(cred->principal_obtained, cred->username_obtained))) { @@ -374,16 +393,180 @@ int cli_credentials_get_keytab(struct cli_credentials *cred, return ret; } - ret = create_memory_keytab(cred, cred, smb_krb5_context, &ktc); + mem_ctx = talloc_new(cred); + if (!mem_ctx) { + return ENOMEM; + } + + ret = create_memory_keytab(mem_ctx, cred, smb_krb5_context, &ktc); if (ret) { + talloc_free(mem_ctx); return ret; } cred->keytab_obtained = (MAX(cred->principal_obtained, cred->username_obtained)); + talloc_steal(cred, ktc); cred->keytab = ktc; *_ktc = cred->keytab; + talloc_free(mem_ctx); + return ret; +} + +/* Given the name of a keytab (presumably in the format + * FILE:/etc/krb5.keytab), open it and attach it */ + +int cli_credentials_set_keytab(struct cli_credentials *cred, + const char *keytab_name, + enum credentials_obtained obtained) +{ + krb5_error_code ret; + struct keytab_container *ktc; + struct smb_krb5_context *smb_krb5_context; + krb5_keytab keytab; + TALLOC_CTX *mem_ctx; + + if (cred->keytab_obtained >= obtained) { + return 0; + } + + ret = cli_credentials_get_krb5_context(cred, &smb_krb5_context); + if (ret) { + return ret; + } + + mem_ctx = talloc_new(cred); + if (!mem_ctx) { + return ENOMEM; + } + + ret = krb5_kt_resolve(smb_krb5_context->krb5_context, keytab_name, &keytab); + if (ret) { + DEBUG(1,("failed to open krb5 keytab: %s\n", + smb_get_krb5_error_message(smb_krb5_context->krb5_context, + ret, mem_ctx))); + talloc_free(mem_ctx); + return ret; + } + + ktc = talloc(mem_ctx, struct keytab_container); + if (!ktc) { + talloc_free(mem_ctx); + return ENOMEM; + } + + ktc->smb_krb5_context = talloc_reference(ktc, smb_krb5_context); + ktc->keytab = keytab; + + cred->keytab_obtained = obtained; + + talloc_steal(cred, ktc); + cred->keytab = ktc; + talloc_free(mem_ctx); + + return ret; +} + +int cli_credentials_update_keytab(struct cli_credentials *cred) +{ + krb5_error_code ret; + struct keytab_container *ktc; + struct smb_krb5_context *smb_krb5_context; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(cred); + if (!mem_ctx) { + return ENOMEM; + } + + ret = cli_credentials_get_krb5_context(cred, &smb_krb5_context); + if (ret) { + talloc_free(mem_ctx); + return ret; + } + + ret = cli_credentials_get_keytab(cred, &ktc); + if (ret != 0) { + talloc_free(mem_ctx); + return ret; + } + + ret = update_keytab(mem_ctx, cred, smb_krb5_context, ktc); + + talloc_free(mem_ctx); + return ret; +} + +/* Get server gss credentials (in gsskrb5, this means the keytab) */ + +int cli_credentials_get_server_gss_creds(struct cli_credentials *cred, + struct gssapi_creds_container **_gcc) +{ + int ret = 0; + OM_uint32 maj_stat, min_stat; + struct gssapi_creds_container *gcc; + struct keytab_container *ktc; + struct smb_krb5_context *smb_krb5_context; + TALLOC_CTX *mem_ctx; + krb5_principal princ; + + if (cred->server_gss_creds_obtained >= (MAX(cred->keytab_obtained, + MAX(cred->principal_obtained, + cred->username_obtained)))) { + *_gcc = cred->server_gss_creds; + return 0; + } + + ret = cli_credentials_get_krb5_context(cred, &smb_krb5_context); + if (ret) { + return ret; + } + + ret = cli_credentials_get_keytab(cred, + &ktc); + if (ret) { + DEBUG(1, ("Failed to get keytab for GSSAPI server: %s\n", error_message(ret))); + return ret; + } + + mem_ctx = talloc_new(cred); + if (!mem_ctx) { + return ENOMEM; + } + + ret = principal_from_credentials(mem_ctx, cred, smb_krb5_context, &princ); + if (ret) { + DEBUG(1,("cli_credentials_get_server_gss_creds: makeing krb5 principal failed (%s)\n", + smb_get_krb5_error_message(smb_krb5_context->krb5_context, + ret, mem_ctx))); + talloc_free(mem_ctx); + return ret; + } + + gcc = talloc(cred, struct gssapi_creds_container); + if (!gcc) { + talloc_free(mem_ctx); + return ENOMEM; + } + + /* This creates a GSSAPI cred_id_t with the principal and keytab set */ + maj_stat = gss_krb5_import_cred(&min_stat, NULL, princ, ktc->keytab, + &gcc->creds); + if (maj_stat) { + if (min_stat) { + ret = min_stat; + } else { + ret = EINVAL; + } + } + if (ret == 0) { + cred->server_gss_creds_obtained = cred->keytab_obtained; + talloc_set_destructor(gcc, free_gssapi_creds); + cred->server_gss_creds = gcc; + *_gcc = gcc; + } + talloc_free(mem_ctx); return ret; } @@ -415,3 +598,5 @@ void cli_credentials_set_salt_principal(struct cli_credentials *cred, const char { cred->salt_principal = talloc_strdup(cred, principal); } + + diff --git a/source4/auth/gensec/gensec_gssapi.c b/source4/auth/gensec/gensec_gssapi.c index b5a2dadd35..68da2567da 100644 --- a/source4/auth/gensec/gensec_gssapi.c +++ b/source4/auth/gensec/gensec_gssapi.c @@ -41,12 +41,9 @@ struct gensec_gssapi_state { DATA_BLOB pac; struct smb_krb5_context *smb_krb5_context; - krb5_ccache ccache; - const char *ccache_name; - struct keytab_container *keytab; struct gssapi_creds_container *client_cred; + struct gssapi_creds_container *server_cred; - gss_cred_id_t cred; gss_cred_id_t delegated_cred_handle; }; @@ -81,10 +78,6 @@ static int gensec_gssapi_destory(void *ptr) struct gensec_gssapi_state *gensec_gssapi_state = ptr; OM_uint32 maj_stat, min_stat; - if (gensec_gssapi_state->cred != GSS_C_NO_CREDENTIAL) { - maj_stat = gss_release_cred(&min_stat, - &gensec_gssapi_state->cred); - } if (gensec_gssapi_state->delegated_cred_handle != GSS_C_NO_CREDENTIAL) { maj_stat = gss_release_cred(&min_stat, &gensec_gssapi_state->delegated_cred_handle); @@ -137,7 +130,6 @@ static NTSTATUS gensec_gssapi_start(struct gensec_security *gensec_security) gensec_gssapi_state->session_key = data_blob(NULL, 0); gensec_gssapi_state->pac = data_blob(NULL, 0); - gensec_gssapi_state->cred = GSS_C_NO_CREDENTIAL; gensec_gssapi_state->delegated_cred_handle = GSS_C_NO_CREDENTIAL; talloc_set_destructor(gensec_gssapi_state, gensec_gssapi_destory); @@ -167,11 +159,10 @@ static NTSTATUS gensec_gssapi_start(struct gensec_security *gensec_security) static NTSTATUS gensec_gssapi_server_start(struct gensec_security *gensec_security) { NTSTATUS nt_status; - OM_uint32 maj_stat, min_stat; int ret; - const char *principal; struct gensec_gssapi_state *gensec_gssapi_state; struct cli_credentials *machine_account; + struct gssapi_creds_container *gcc; nt_status = gensec_gssapi_start(gensec_security); if (!NT_STATUS_IS_OK(nt_status)) { @@ -186,53 +177,15 @@ static NTSTATUS gensec_gssapi_server_start(struct gensec_security *gensec_securi DEBUG(3, ("No machine account credentials specified\n")); return NT_STATUS_INVALID_PARAMETER; } else { - ret = cli_credentials_get_keytab(machine_account, &gensec_gssapi_state->keytab); + ret = cli_credentials_get_server_gss_creds(machine_account, &gcc); if (ret) { - DEBUG(3, ("Could not create memory keytab!\n")); + DEBUG(1, ("Aquiring acceptor credentials failed: %s\n", + error_message(ret))); return NT_STATUS_CANT_ACCESS_DOMAIN_INFO; } } - principal = cli_credentials_get_principal(machine_account, - machine_account); - - /* This might have been explicity set to NULL, ie use what the client calls us */ - if (principal) { - gss_buffer_desc name_token; - - name_token.value = discard_const_p(uint8_t, principal); - name_token.length = strlen(principal); - - maj_stat = gss_import_name (&min_stat, - &name_token, - GSS_C_NT_USER_NAME, - &gensec_gssapi_state->server_name); - - if (maj_stat) { - DEBUG(2, ("GSS Import name of %s failed: %s\n", - (char *)name_token.value, - gssapi_error_string(gensec_gssapi_state, maj_stat, min_stat))); - return NT_STATUS_UNSUCCESSFUL; - } - } else { - gensec_gssapi_state->server_name = GSS_C_NO_NAME; - } - - maj_stat = gsskrb5_acquire_cred(&min_stat, - gensec_gssapi_state->keytab->keytab, - gensec_gssapi_state->server_name, - GSS_C_INDEFINITE, - GSS_C_NULL_OID_SET, - GSS_C_ACCEPT, - &gensec_gssapi_state->cred, - NULL, - NULL); - if (maj_stat) { - DEBUG(1, ("Aquiring acceptor credentails failed: %s\n", - gssapi_error_string(gensec_gssapi_state, maj_stat, min_stat))); - return NT_STATUS_CANT_ACCESS_DOMAIN_INFO; - } - + gensec_gssapi_state->server_cred = gcc; return NT_STATUS_OK; } @@ -382,7 +335,7 @@ static NTSTATUS gensec_gssapi_update(struct gensec_security *gensec_security, { maj_stat = gss_accept_sec_context(&min_stat, &gensec_gssapi_state->gssapi_context, - gensec_gssapi_state->cred, + gensec_gssapi_state->server_cred->creds, &input_token, gensec_gssapi_state->input_chan_bindings, &gensec_gssapi_state->client_name, -- cgit