From: Gary Lockyer Date: Mon, 29 Sep 2025 23:25:51 +0000 (+1300) Subject: third_party:heimdal: import lorikeet-heimdal-202509242121 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8cb97336ac3955cdea2cf81c141148c1b258abd7;p=thirdparty%2Fsamba.git third_party:heimdal: import lorikeet-heimdal-202509242121 (commit beffefde5c6767589603cca98065378250eaae2c) Changes to heimdal to implement Windows strong and flexible certificate mapping as outlined in KB5014754: Certificate-based authentication changes on Windows domain controllers https://support.microsoft.com/en-us/topic/kb5014754-certificate-based- authentication-changes-on-windows- domain-controllers- ad2c23b0-15d8-4340-a468-4d4f3b188f16 Signed-off-by: Gary Lockyer Reviewed-by: Jennifer Sutton Autobuild-User(master): Jennifer Sutton Autobuild-Date(master): Wed Oct 8 21:12:44 UTC 2025 on atb-devel-224 --- diff --git a/third_party/heimdal/.gitignore b/third_party/heimdal/.gitignore index 6a56c208980..aecf4f611a4 100644 --- a/third_party/heimdal/.gitignore +++ b/third_party/heimdal/.gitignore @@ -361,9 +361,13 @@ asn1_*_asn1.c /lib/hdb/asn1_HDB_EncTypeList.c /lib/hdb/asn1_HDB_EntryOrAlias.c /lib/hdb/asn1_HDB_Ext_Aliases.c +/lib/hdb/asn1_HDB_Ext_CertificateMapping.c +/lib/hdb/asn1_HDB_Ext_CertificateMappings.c /lib/hdb/asn1_HDB_Ext_Constrained_delegation_acl.c +/lib/hdb/asn1_HDB_Ext_EnforcementMode.c /lib/hdb/asn1_HDB_Ext_KeyRotation.c /lib/hdb/asn1_HDB_Ext_KeySet.c +/lib/hdb/asn1_HDB_Ext_KeyTrust.c /lib/hdb/asn1_HDB_Ext_Lan_Manager_OWF.c /lib/hdb/asn1_HDB_Ext_PKINIT_acl.c /lib/hdb/asn1_HDB_Ext_PKINIT_cert.c diff --git a/third_party/heimdal/kdc/kerberos5.c b/third_party/heimdal/kdc/kerberos5.c index d0c646c328c..80048109493 100644 --- a/third_party/heimdal/kdc/kerberos5.c +++ b/third_party/heimdal/kdc/kerberos5.c @@ -563,6 +563,8 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa) ret == KRB5_KDC_ERR_CLIENT_NOT_TRUSTED) { ret = KRB5_KDC_ERR_CLIENT_NOT_TRUSTED; + } else if (ret == KRB5_KDC_ERR_CERTIFICATE_MISMATCH) { + ret = KRB5_KDC_ERR_CERTIFICATE_MISMATCH; } else { ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; } diff --git a/third_party/heimdal/kdc/pkinit.c b/third_party/heimdal/kdc/pkinit.c index 6fdae939f8c..ea734dd50c7 100644 --- a/third_party/heimdal/kdc/pkinit.c +++ b/third_party/heimdal/kdc/pkinit.c @@ -35,7 +35,6 @@ #include "hdb_asn1.h" #include "kdc_locl.h" - #ifdef PKINIT #include @@ -427,7 +426,7 @@ pk_check_key_trust( krb5_context context, hdb_entry *client, hx509_cert *cert) ret = KRB5_KDC_ERR_CLIENT_NOT_TRUSTED; krb5_set_error_message( context, ret, "Unable to get certificate public key"); - goto out1; + goto out; } /* @@ -439,7 +438,7 @@ pk_check_key_trust( krb5_context context, hdb_entry *client, hx509_cert *cert) if (ret) { krb5_set_error_message( context, ret, "Unable to encode certificate public key"); - goto out2; + goto out1; } keys = ext->data.u.key_trust; @@ -456,10 +455,346 @@ pk_check_key_trust( krb5_context context, hdb_entry *client, hx509_cert *cert) } der_free_octet_string(&buf); -out2: - free_SubjectPublicKeyInfo(&spki); out1: - free_HDB_extension(ext); + free_SubjectPublicKeyInfo(&spki); +out: + return ret; +} + +/** + * @brief Match the target name against the value on the certificate + * + * name is converted to it's rfc4514 form and compared to the target + * + * @param[in] context krb5 context + * @param[in] name name on the certificate + * @param[in] target value to match name against + * + * @return TRUE the name matches + * FALSE the name DOES NOT match + */ +static krb5_boolean match_name( + const krb5_context context, + const hx509_name *name, + const heim_octet_string *target) +{ + krb5_boolean matched = FALSE; + char *ns = NULL; + char *ts = NULL; + krb5_error_code ret = 0; + + ret = hx509_name_to_string(*name, &ns); + if (ret != 0) { + return FALSE; + } + + ts = calloc(target->length + 1, sizeof(char)); + if (ts == NULL) { + goto out; + } + memcpy(ts, target->data, target->length); + if (strncmp(ts, ns, target->length) == 0) { + matched = TRUE; + } + free(ts); + +out: + free(ns); + return matched; +} + +/** + * @brief does the rfc822 name of the certificate match the value in mapping? + * + * @param[in] context krb5 context + * @param[in] cert X509 certificate + * @param[in] m certificate mapping + * + * @return TRUE the certificate matches + * FALSE the certificate DOES NOT match + */ +static krb5_boolean +match_rfc822_name( + const krb5_context context, + const hx509_cert *cert, + const HDB_Ext_CertificateMapping *m) +{ + krb5_error_code ret = 0; + krb5_boolean matched = FALSE; + size_t j = 0; + hx509_octet_string_list list; + + ret = hx509_cert_find_subjectAltName_rfc822(context->hx509ctx, + *cert, + &list); + if (ret != 0) { + return FALSE; + } + + for (j = 0, matched = FALSE; j < list.len && !matched; j++) { + if (list.val[j].length == m->rfc822->length && + memcmp(m->rfc822->data, list.val[j].data, list.val[j].length) == 0) + { + matched = TRUE; + } + } + hx509_free_octet_string_list(&list); + return matched; +} + +/** + * @brief does the SHA1 hash of the certificate public key match the + * value in mapping? + * + * @param[in] context krb5 context + * @param[in] cert X509 certificate + * @param[in] m certificate mapping + * + * @return TRUE the certificate matches + * FALSE the certificate DOES NOT match + */ +static krb5_boolean +match_public_key( + const krb5_context context, + const hx509_cert *cert, + const HDB_Ext_CertificateMapping *m) +{ + krb5_error_code ret = 0; + krb5_boolean matched = FALSE; + unsigned char digest[EVP_MAX_MD_SIZE]; + EVP_MD_CTX *ctx = NULL; + unsigned int size = 0; + + SubjectPublicKeyInfo spki; + ret = hx509_cert_get_SPKI(context->hx509ctx, *cert, &spki); + if (ret != 0) { + return FALSE; + } + + /* + * Compute the SHA1 hash of the certificate subject public key + */ + ctx = EVP_MD_CTX_create(); + if (ctx == NULL) { + goto out; + } + EVP_DigestInit_ex(ctx, EVP_sha1(), NULL); + EVP_DigestUpdate( + ctx, spki.subjectPublicKey.data, spki.subjectPublicKey.length/8); + EVP_DigestFinal_ex(ctx, digest, &size); + EVP_MD_CTX_destroy(ctx); + + /* + * Now compare them + */ + if (size == m->public_key->length && + memcmp(digest, m->public_key->data, m->public_key->length) == 0) { + matched = TRUE; + } + +out: + free_SubjectPublicKeyInfo(&spki); + return matched; +} + +/** + * @brief does the certificate SKI match the SKI in mapping? + * + * @param[in] context krb5 context + * @param[in] cert X509 certificate + * @param[in] m certificate mapping + * + * @return TRUE the certificate matches + * FALSE the certificate DOES NOT match + */ +static krb5_boolean +match_subject_key_identifier( + const krb5_context context, + const hx509_cert *cert, + const HDB_Ext_CertificateMapping *m) +{ + krb5_error_code ret = 0; + krb5_boolean matched = FALSE; + + SubjectKeyIdentifier ski; + ret = hx509_cert_get_subject_key_identifier(context->hx509ctx, *cert, &ski); + if (ret != 0) { + return FALSE; + } + if (der_heim_octet_string_cmp(m->ski, &ski) == 0) { + matched = TRUE; + } + free_SubjectKeyIdentifier(&ski); + return matched; +} + +/** + * @brief does the certificate serial number match the serial number in mapping? + * + * @param[in] cert X509 certificate + * @param[in] m certificate mapping + * + * @return TRUE the certificate matches + * FALSE the certificate DOES NOT match + */ +static krb5_boolean +match_serial_number( + const hx509_cert *cert, + const HDB_Ext_CertificateMapping *m) +{ + krb5_error_code ret = 0; + krb5_boolean matched = FALSE; + heim_integer serial_number; + + ret = hx509_cert_get_serialnumber(*cert, &serial_number); + if (ret != 0) { + return FALSE; + } + if (serial_number.length == m->serial_number->length && + memcmp(serial_number.data, + m->serial_number->data, + serial_number.length) == 0) { + matched = TRUE; + } + der_free_heim_integer(&serial_number); + return matched; +} + +/** + * @brief Validate the certificate against the criteria outlined in KB5014754 + * + * @see KB5014754: Certificate-based authentication changes on Windows domain + * controllers + * https://support.microsoft.com/en-us/topic/ + * kb5014754-certificate-based-authentication-changes-on-windows + * -domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 + * + * @param context[in] krb5 context + * @param client[in] client hdb record + * @param cert[in] X509 certificate used to sign the request. + * + * @return 0 no error + * KRB5_KDC_ERR_CLIENT_NOT_TRUSTED certificate fails KB5014754 + * otherwise an error occurred processing the request. + */ +static krb5_error_code +pk_check_certificate_binding( + krb5_context context, + hdb_entry *client, + hx509_cert *cert) +{ + + krb5_error_code ret = 0; + HDB_extension *ext = NULL; + HDB_Ext_CertificateMappings mappings; + unsigned int i = 0; + krb5_boolean matched = FALSE; + krb5_boolean strong_mapping = FALSE; + + memset(&mappings, 0, sizeof(mappings)); + + /* + * If there is no extension or the enforcement mode is none + * then there is nothing to do. + */ + ext = hdb_find_extension(client, choice_HDB_extension_data_cert_mappings); + if (ext == NULL) { + return 0; + } + mappings = ext->data.u.cert_mappings; + if (mappings.enforcement_mode == hdb_enf_mode_none) { + ret = 0; + goto out; + } + + /* + * If there are no mappings then reject the logon + */ + if (mappings.mappings == NULL) { + ret = KRB5_KDC_ERR_CERTIFICATE_MISMATCH; + krb5_set_error_message( + context, ret, "Client has no certificate mappings"); + goto out; + } + + for (i = 0, matched = FALSE; i < mappings.mappings->len && !matched; i++) { + HDB_Ext_CertificateMapping *m = &mappings.mappings->val[i]; + + strong_mapping = m->strong_mapping; + /* + * When enforcement mode is full only consider strong mappings + */ + if (mappings.enforcement_mode == hdb_enf_mode_full && !strong_mapping) { + continue; + } + + if (m->issuer_name != NULL) { + hx509_name issuer; + ret = hx509_cert_get_issuer(*cert, &issuer); + if (ret != 0) { + continue; + } + matched = match_name(context, &issuer, m->issuer_name); + hx509_name_free(&issuer); + if (!matched) { + continue; + } + } + if (m->subject_name != NULL) { + hx509_name subject; + ret = hx509_cert_get_subject(*cert, &subject); + if (ret != 0) { + continue; + } + matched = match_name(context, &subject, m->subject_name); + hx509_name_free(&subject); + if (!matched) { + continue; + } + } + if (m->rfc822 != NULL) { + matched = match_rfc822_name(context, cert, m); + if (!matched) { + continue; + } + } + if (m->ski != NULL) { + matched = match_subject_key_identifier(context, cert, m); + if (!matched) { + continue; + } + } + if (m->public_key != NULL) { + matched = match_public_key(context, cert, m); + if (!matched) { + continue; + } + } + if (m->serial_number != NULL) { + matched = match_serial_number(cert, m); + if (!matched) { + continue; + } + } + } + + /* + * When enforcement mode is compatibility need to consider + * the age of the certificate for weak mappings + */ + if (mappings.enforcement_mode == hdb_enf_mode_compatibility && + matched && + !strong_mapping) { + + time_t certificate_start = hx509_cert_get_notBefore(*cert); + if (mappings.valid_certificate_start > certificate_start) { + matched = FALSE; + } + } + if (!matched) { + krb5_warnx(context, "PKINIT: No matching certificate mappings"); + ret = KRB5_KDC_ERR_CERTIFICATE_MISMATCH; + } out: return ret; } @@ -723,6 +1058,10 @@ _kdc_pk_rd_padata(astgs_request_t priv, if (ret) { goto out; } + ret = pk_check_certificate_binding(context, client, &cp->cert); + if (ret) { + goto out; + } } else if (ret == HX509_CMS_NO_RECIPIENT_CERTIFICATE || ret == HX509_ISSUER_NOT_FOUND) { /* diff --git a/third_party/heimdal/lib/hdb/Makefile.am b/third_party/heimdal/lib/hdb/Makefile.am index d36e6faecd3..5be8d97f9c6 100644 --- a/third_party/heimdal/lib/hdb/Makefile.am +++ b/third_party/heimdal/lib/hdb/Makefile.am @@ -27,6 +27,9 @@ gen_files_hdb = \ asn1_HDB_EncTypeList.c \ asn1_HDB_Ext_Aliases.c \ asn1_HDB_Ext_Constrained_delegation_acl.c \ + asn1_HDB_Ext_CertificateMapping.c \ + asn1_HDB_Ext_CertificateMappings.c \ + asn1_HDB_Ext_EnforcementMode.c \ asn1_HDB_Ext_KeyRotation.c \ asn1_HDB_Ext_KeySet.c \ asn1_HDB_Ext_KeyTrust.c \ diff --git a/third_party/heimdal/lib/hdb/hdb.asn1 b/third_party/heimdal/lib/hdb/hdb.asn1 index 9102e2b8f94..c890334df63 100644 --- a/third_party/heimdal/lib/hdb/hdb.asn1 +++ b/third_party/heimdal/lib/hdb/hdb.asn1 @@ -196,6 +196,31 @@ HDB-Ext-KeyTrust ::= SEQUENCE OF SEQUENCE { pub_key[0] OCTET STRING } +HDB-Ext-CertificateMapping ::= SEQUENCE { + strong-mapping[0] BOOLEAN, + issuer-name[1] OCTET STRING OPTIONAL, + subject-name[2] OCTET STRING OPTIONAL, + serial-number[3] OCTET STRING OPTIONAL, + ski[4] OCTET STRING OPTIONAL, + public-key[5] OCTET STRING OPTIONAL, + rfc822[6] OCTET STRING OPTIONAL, + ... +} + +HDB-Ext-EnforcementMode ::= ENUMERATED { + hdb_enf_mode_none (0), + hdb_enf_mode_compatibility (1), + hdb_enf_mode_full (2), + ... +} + +HDB-Ext-CertificateMappings ::= SEQUENCE { + enforcement_mode[0] HDB-Ext-EnforcementMode, + valid_certificate_start[1] KerberosTime, + mappings[2] SEQUENCE OF HDB-Ext-CertificateMapping OPTIONAL, + ... +} + HDB-extension ::= SEQUENCE { mandatory[0] BOOLEAN, -- kdc MUST understand this extension, -- if not the whole entry must @@ -218,6 +243,7 @@ HDB-extension ::= SEQUENCE { key-rotation[14] HDB-Ext-KeyRotation, krb5-config[15] OCTET STRING, key-trust[16] HDB-Ext-KeyTrust, + cert-mappings[17] HDB-Ext-CertificateMappings, ... }, ... diff --git a/third_party/heimdal/lib/hdb/version-script.map b/third_party/heimdal/lib/hdb/version-script.map index 6ef67410b2d..5e17e8b3813 100644 --- a/third_party/heimdal/lib/hdb/version-script.map +++ b/third_party/heimdal/lib/hdb/version-script.map @@ -160,6 +160,9 @@ HEIMDAL_HDB_1.0 { free_HDB_Ext_KeyRotation; free_HDB_Ext_KeySet; free_HDB_Ext_KeyTrust; + free_HDB_Ext_KeyTrust; + free_HDB_Ext_CertificateMapping; + free_HDB_Ext_CertificateMappings; free_HDB_Ext_PKINIT_acl; free_hdb_keyset; free_HDB_keyset; diff --git a/third_party/heimdal/lib/hx509/cert.c b/third_party/heimdal/lib/hx509/cert.c index 9c7997dc46e..0034416e9fe 100644 --- a/third_party/heimdal/lib/hx509/cert.c +++ b/third_party/heimdal/lib/hx509/cert.c @@ -33,6 +33,7 @@ #include "hx_locl.h" #include "crypto-headers.h" +#include "rfc2459_asn1.h" #include /** @@ -964,6 +965,63 @@ hx509_cert_find_subjectAltName_otherName(hx509_context context, } } +/** + * @brief Return a list of rfc822 subject alternative names (email addresses) + * + * @param[in] context hx509 context. + * @param[in] cert hx509 certificate + * @param[out] list of rfc822 subjectAltNames. + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @ingroup hx509_cert + * + * @note The returned list of octet string should be freed with + * hx509_free_octet_string_list(). + * + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_cert_find_subjectAltName_rfc822(hx509_context context, + hx509_cert cert, + hx509_octet_string_list *list) +{ + GeneralNames sa; + int ret = 0; + size_t i = 0; + size_t j = 0; + + list->val = NULL; + list->len = 0; + + i = 0; + while (1) { + ret = find_extension_subject_alt_name(_hx509_get_cert(cert), &i, &sa); + if (ret == HX509_EXTENSION_NOT_FOUND) { + return 0; + } else if (ret != 0) { + hx509_set_error_string( + context, 0, ret, "Error searching for RFC822 names"); + hx509_free_octet_string_list(list); + return ret; + } + + for (j = 0; j < sa.len; j++) { + if (sa.val[j].element == choice_GeneralName_rfc822Name) { + ret = add_to_list(list, &sa.val[j].u.rfc822Name); + if (ret) { + hx509_set_error_string(context, 0, ret, + "Error adding an extra RFC822 name " + "to return list"); + hx509_free_octet_string_list(list); + free_GeneralNames(&sa); + return ret; + } + } + } + free_GeneralNames(&sa); + } +} + static int check_key_usage(hx509_context context, const Certificate *cert, @@ -1867,6 +1925,40 @@ hx509_cert_get_subject_unique_id(hx509_context context, hx509_cert p, heim_bit_s return get_x_unique_id(context, "subject", p->data->tbsCertificate.subjectUniqueID, subject); } +/** + * @brief get the (SKI) Subject Key Identifier from a certificate + * + * @param[in] context hx509 context + * @param[in] cert hx509 certificate + * @param[out] ski subject key identifier + * + * @return An hx509 error code, see hx509_get_error_string(). + * + * @note ski should be freed with a call to free_SubjectKeyIdentifier + */ +HX509_LIB_FUNCTION int HX509_LIB_CALL +hx509_cert_get_subject_key_identifier(hx509_context context, + hx509_cert cert, + SubjectKeyIdentifier *ski) +{ + size_t i = 0; + size_t size = 0; + const Extension *ext = NULL; + int ret = 0; + + ext = find_extension(cert->data, + &asn1_oid_id_x509_ce_subjectKeyIdentifier, + &i); + if (ext == NULL) { + return HX509_EXTENSION_NOT_FOUND; + } + ret = decode_SubjectKeyIdentifier(ext->extnValue.data, + ext->extnValue.length, + ski, + &size); + return ret; +} + HX509_LIB_FUNCTION hx509_private_key HX509_LIB_CALL _hx509_cert_private_key(hx509_cert p) diff --git a/third_party/heimdal/lib/hx509/version-script.map b/third_party/heimdal/lib/hx509/version-script.map index 27f5f51f767..9ba621d68a2 100644 --- a/third_party/heimdal/lib/hx509/version-script.map +++ b/third_party/heimdal/lib/hx509/version-script.map @@ -95,6 +95,7 @@ HEIMDAL_X509_1.2 { hx509_cert_check_eku; hx509_cert_cmp; hx509_cert_find_subjectAltName_otherName; + hx509_cert_find_subjectAltName_rfc822; hx509_cert_free; hx509_cert_get_SPKI; hx509_cert_get_SPKI_AlgorithmIdentifier; @@ -109,6 +110,7 @@ HEIMDAL_X509_1.2 { hx509_cert_get_subject; hx509_cert_get_issuer_unique_id; hx509_cert_get_subject_unique_id; + hx509_cert_get_subject_key_identifier; hx509_cert_have_private_key; hx509_cert_have_private_key_only; hx509_cert_init; diff --git a/third_party/heimdal/lib/krb5/krb5_err.et b/third_party/heimdal/lib/krb5/krb5_err.et index 2b4062e6abd..1294c1e4bd0 100644 --- a/third_party/heimdal/lib/krb5/krb5_err.et +++ b/third_party/heimdal/lib/krb5/krb5_err.et @@ -79,6 +79,7 @@ error_code CLIENT_NOT_TRUSTED, "Client not trusted" error_code KDC_NOT_TRUSTED, "KDC not trusted" error_code INVALID_SIG, "Invalid signature" error_code DH_KEY_PARAMETERS_NOT_ACCEPTED, "DH parameters not accepted" +error_code CERTIFICATE_MISMATCH, "The incorrect certificate was used" index 68 prefix KRB5_KDC_ERR