CVE-2022-37967 Add new PAC checksum
authorJoseph Sutton <josephsutton@catalyst.net.nz>
Wed, 9 Nov 2022 00:45:13 +0000 (13:45 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 28 Nov 2023 22:46:37 +0000 (11:46 +1300)
BUG: https://bugzilla.samba.org/show_bug.cgi?id=15231

Pair-Programmed-With: Andrew Bartlett <abartlet@samba.org>

Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Stefan Metzmacher <metze@samba.org>
[abartlet@samba.org Amended in lorikeet-heimdal to make test_pac continue to pass]

kdc/kerberos5.c
kdc/krb5tgs.c
lib/krb5/pac.c
lib/krb5/test_pac.c

index 6e8a281574ac155535b68d5bde4ee67d1485c296..7b7cce5e839a863f3df9d15f708b45de1f2781d6 100644 (file)
@@ -2144,6 +2144,7 @@ generate_pac(astgs_request_t r, const Key *skey, const Key *tkey,
                         rodc_id,
                         NULL, /* UPN */
                         canon_princ,
+                        FALSE, /* add_full_sig */
                         is_tgs ? &r->pac_attributes : NULL,
                         &data);
     krb5_free_principal(r->context, client);
index 2b7caac717dfd812d8c3bf9f5c613150ff80c20d..7a64802863372c8c9ad27077b3cd16fad749b0ad 100644 (file)
@@ -779,7 +779,7 @@ tgs_make_reply(astgs_request_t r,
 
            ret = _krb5_kdc_pac_sign_ticket(r->context, r->pac, r->client_princ, serverkey,
                                            krbtgtkey, rodc_id, NULL, r->canon_client_princ,
-                                           add_ticket_sig, et,
+                                           add_ticket_sig, add_ticket_sig, et,
                                            is_tgs ? &r->pac_attributes : NULL);
            if (ret)
                goto out;
index f2f8803eade722c5e30ccf6d127ea4e5a7e16e5e..6e0877e1819b55207c4296c92774a2337c95afa5 100644 (file)
@@ -74,6 +74,7 @@ struct krb5_pac_data {
     struct PAC_INFO_BUFFER *upn_dns_info;
     struct PAC_INFO_BUFFER *ticket_checksum;
     struct PAC_INFO_BUFFER *attributes_info;
+    struct PAC_INFO_BUFFER *full_checksum;
     krb5_data ticket_sign_data;
 
     /* PAC_UPN_DNS_INFO */
@@ -101,6 +102,7 @@ struct krb5_pac_data {
 #define PAC_TICKET_CHECKSUM            16
 #define PAC_ATTRIBUTES_INFO            17
 #define PAC_REQUESTOR_SID              18
+#define PAC_FULL_CHECKSUM              19
 
 /* Flag in PAC_UPN_DNS_INFO */
 #define PAC_EXTRA_LOGON_INFO_FLAGS_UPN_DEFAULTED       0x1
@@ -398,6 +400,13 @@ krb5_pac_parse(krb5_context context, const void *ptr, size_t len,
             else
                 p->attributes_info = &p->pac->buffers[i];
             break;
+        case PAC_FULL_CHECKSUM:
+           if (p->full_checksum)
+               krb5_set_error_message(context, ret = EINVAL,
+                                      N_("PAC has multiple full checksums", ""));
+            else
+                p->full_checksum = &p->pac->buffers[i];
+            break;
         default: break;
         }
     }
@@ -668,7 +677,8 @@ verify_checksum(krb5_context context,
                const struct PAC_INFO_BUFFER *sig,
                const krb5_data *data,
                void *ptr, size_t len,
-               const krb5_keyblock *key)
+               const krb5_keyblock *key,
+               krb5_boolean strict_cksumtype_match)
 {
     krb5_storage *sp = NULL;
     uint32_t type;
@@ -726,7 +736,7 @@ verify_checksum(krb5_context context,
      * http://blogs.msdn.com/b/openspecification/archive/2010/01/01/verifying-the-server-signature-in-kerberos-privilege-account-certificate.aspx
      * for Microsoft's explaination */
 
-    if (cksum.cksumtype == CKSUMTYPE_HMAC_MD5) {
+    if (cksum.cksumtype == CKSUMTYPE_HMAC_MD5 && !strict_cksumtype_match) {
        Checksum local_checksum;
 
        memset(&local_checksum, 0, sizeof(local_checksum));
@@ -1192,7 +1202,7 @@ parse_attributes_info(krb5_context context,
  * @param pac the pac structure returned by krb5_pac_parse().
  * @param authtime The time of the ticket the PAC belongs to.
  * @param principal the principal to verify.
- * @param server The service key, most always be given.
+ * @param server The service key, may be given.
  * @param privsvr The KDC key, may be given.
 
  * @return Returns 0 to indicate success. Otherwise an kerberos et
@@ -1210,6 +1220,21 @@ krb5_pac_verify(krb5_context context,
                const krb5_keyblock *privsvr)
 {
     krb5_error_code ret;
+    /*
+     * If we are in the KDC, we expect back a full signature in the PAC
+     *
+     * This is set up as a separate variable to make it easier if a
+     * subsequent patch is added to make this configurable in the
+     * krb5.conf (or forced into the krb5_context via Samba)
+     */
+    krb5_boolean expect_full_sig = privsvr != NULL;
+
+    /*
+     * If we are on the KDC, then we trust we are not in a realm with
+     * buggy Windows 2008 or similar era DCs that give out HMAC-MD5
+     * signatures over AES keys.  DES is also already gone.
+     */
+    krb5_boolean strict_cksumtype_match = expect_full_sig;
 
     if (pac->server_checksum == NULL) {
        krb5_set_error_message(context, EINVAL, "PAC missing server checksum");
@@ -1223,6 +1248,10 @@ krb5_pac_verify(krb5_context context,
        krb5_set_error_message(context, EINVAL, "PAC missing logon name");
        return EINVAL;
     }
+    if (expect_full_sig && pac->full_checksum == NULL) {
+       krb5_set_error_message(context, EINVAL, "PAC missing full checksum");
+       return EINVAL;
+    }
 
     if (principal != NULL) {
        ret = verify_logonname(context, pac->logon_name, &pac->data, authtime,
@@ -1235,14 +1264,15 @@ krb5_pac_verify(krb5_context context,
         pac->privsvr_checksum->buffersize < 4)
        return EINVAL;
 
-    /*
-     * in the service case, clean out data option of the privsvr and
-     * server checksum before checking the checksum.
-     */
-    if (server != NULL)
+    if (server != NULL || privsvr != NULL)
     {
        krb5_data *copy;
 
+       /*
+        * in the service case, clean out data option of the privsvr and
+        * server checksum before checking the checksum.
+        */
+
        ret = krb5_copy_data(context, &pac->data, &copy);
        if (ret)
            return ret;
@@ -1255,15 +1285,43 @@ krb5_pac_verify(krb5_context context,
               0,
               pac->privsvr_checksum->buffersize - 4);
 
-       ret = verify_checksum(context,
-                             pac->server_checksum,
-                             &pac->data,
-                             copy->data,
-                             copy->length,
-                             server);
+       if (server != NULL) {
+           ret = verify_checksum(context,
+                                 pac->server_checksum,
+                                 &pac->data,
+                                 copy->data,
+                                 copy->length,
+                                 server,
+                                 strict_cksumtype_match);
+           if (ret) {
+               krb5_free_data(context, copy);
+               return ret;
+           }
+       }
+
+       if (privsvr != NULL && pac->full_checksum != NULL) {
+           /*
+            * in the full checksum case, also clean out the full
+            * checksum before verifying it.
+            */
+           memset((char *)copy->data + pac->full_checksum->offset + 4,
+                  0,
+                  pac->full_checksum->buffersize - 4);
+
+           ret = verify_checksum(context,
+                                 pac->full_checksum,
+                                 &pac->data,
+                                 copy->data,
+                                 copy->length,
+                                 privsvr,
+                                 strict_cksumtype_match);
+           if (ret) {
+               krb5_free_data(context, copy);
+               return ret;
+           }
+       }
+
        krb5_free_data(context, copy);
-       if (ret)
-           return ret;
     }
     if (privsvr) {
        /* The priv checksum covers the server checksum */
@@ -1273,7 +1331,8 @@ krb5_pac_verify(krb5_context context,
                              (char *)pac->data.data
                              + pac->server_checksum->offset + 4,
                              pac->server_checksum->buffersize - 4,
-                             privsvr);
+                             privsvr,
+                             strict_cksumtype_match);
        if (ret)
            return ret;
 
@@ -1286,7 +1345,8 @@ krb5_pac_verify(krb5_context context,
 
            ret = verify_checksum(context, pac->ticket_checksum, &pac->data,
                                 pac->ticket_sign_data.data,
-                                pac->ticket_sign_data.length, privsvr);
+                                pac->ticket_sign_data.length, privsvr,
+                                strict_cksumtype_match);
            if (ret)
                return ret;
        }
@@ -1377,6 +1437,7 @@ _krb5_pac_sign(krb5_context context,
               uint16_t rodc_id,
               krb5_const_principal upn_princ,
               krb5_const_principal canon_princ,
+              krb5_boolean add_full_sig,
               uint64_t *pac_attributes, /* optional */
               krb5_data *data)
 {
@@ -1384,7 +1445,7 @@ _krb5_pac_sign(krb5_context context,
     krb5_storage *sp = NULL, *spdata = NULL;
     uint32_t end;
     size_t server_size, priv_size;
-    uint32_t server_offset = 0, priv_offset = 0, ticket_offset = 0;
+    uint32_t server_offset = 0, priv_offset = 0, ticket_offset = 0, full_offset = 0;
     uint32_t server_cksumtype = 0, priv_cksumtype = 0;
     uint32_t num = 0;
     uint32_t i, sz;
@@ -1461,6 +1522,16 @@ _krb5_pac_sign(krb5_context context,
                                       N_("PAC has multiple attributes info buffers", ""));
                goto out;
            }
+       } else if (p->pac->buffers[i].type == PAC_FULL_CHECKSUM) {
+           if (p->full_checksum == NULL) {
+               p->full_checksum = &p->pac->buffers[i];
+           }
+           if (p->full_checksum != &p->pac->buffers[i]) {
+               ret = KRB5KDC_ERR_BADOPTION;
+               krb5_set_error_message(context, ret,
+                                      N_("PAC has multiple full checksums", ""));
+               goto out;
+           }
        }
     }
 
@@ -1473,6 +1544,8 @@ _krb5_pac_sign(krb5_context context,
        num++;
     if (p->ticket_sign_data.length != 0 && p->ticket_checksum == NULL)
        num++;
+    if (add_full_sig && p->full_checksum == NULL)
+       num++;
 
     /* Allocate any missing-but-necessary buffers */
     if (num) {
@@ -1515,6 +1588,11 @@ _krb5_pac_sign(krb5_context context,
            p->ticket_checksum = &p->pac->buffers[p->pac->numbuffers++];
            p->ticket_checksum->type = PAC_TICKET_CHECKSUM;
        }
+       if (add_full_sig && p->full_checksum == NULL) {
+           p->full_checksum = &p->pac->buffers[p->pac->numbuffers++];
+           memset(p->full_checksum, 0, sizeof(*p->full_checksum));
+           p->full_checksum->type = PAC_FULL_CHECKSUM;
+       }
     }
 
     /* Calculate LOGON NAME */
@@ -1647,6 +1725,31 @@ _krb5_pac_sign(krb5_context context,
                len += sizeof(rodc_id);
                CHECK(ret, krb5_store_uint16(spdata, rodc_id), out);
            }
+       } else if (add_full_sig &&
+                  p->pac->buffers[i].type == PAC_FULL_CHECKSUM) {
+           if (priv_size > UINT32_MAX - 4) {
+               ret = EINVAL;
+               krb5_set_error_message(context, ret, "integer overrun");
+               goto out;
+           }
+           len = priv_size + 4;
+           if (end > UINT32_MAX - 4) {
+               ret = EINVAL;
+               krb5_set_error_message(context, ret, "integer overrun");
+               goto out;
+           }
+           full_offset = end + 4;
+           CHECK(ret, krb5_store_uint32(spdata, priv_cksumtype), out);
+           CHECK(ret, fill_zeros(context, spdata, priv_size), out);
+           if (rodc_id != 0) {
+               if (len > UINT32_MAX - sizeof(rodc_id)) {
+                   ret = EINVAL;
+                   krb5_set_error_message(context, ret, "integer overrun");
+                   goto out;
+               }
+               len += sizeof(rodc_id);
+               CHECK(ret, fill_zeros(context, spdata, sizeof(rodc_id)), out);
+           }
        } else if (p->pac->buffers[i].type == PAC_LOGON_NAME) {
            len = krb5_storage_write(spdata, logon.data, logon.length);
            if (logon.length != len) {
@@ -1708,6 +1811,21 @@ _krb5_pac_sign(krb5_context context,
                              p->ticket_sign_data.data,
                              p->ticket_sign_data.length,
                              (char *)d.data + ticket_offset, priv_size);
+    if (ret == 0 && add_full_sig)
+        ret = create_checksum(context, priv_key, priv_cksumtype,
+                              d.data, d.length,
+                              (char *)d.data + full_offset, priv_size);
+    if (ret == 0 && add_full_sig && rodc_id != 0) {
+       void *buf = (char *)d.data + full_offset + priv_size;
+       krb5_storage *rs = krb5_storage_from_mem(buf, sizeof(rodc_id));
+       if (rs == NULL)
+           ret = krb5_enomem(context);
+        else
+            krb5_storage_set_flags(rs, KRB5_STORAGE_BYTEORDER_LE);
+        if (ret == 0)
+            ret = krb5_store_uint16(rs, rodc_id);
+       krb5_storage_free(rs);
+    }
     if (ret == 0)
         ret = create_checksum(context, server_key, server_cksumtype,
                               d.data, d.length,
@@ -1717,22 +1835,15 @@ _krb5_pac_sign(krb5_context context,
                               (char *)d.data + server_offset, server_size,
                               (char *)d.data + priv_offset, priv_size);
     if (ret == 0 && rodc_id != 0) {
-       krb5_data rd;
-       krb5_storage *rs = krb5_storage_emem();
+       void *buf = (char *)d.data + priv_offset + priv_size;
+       krb5_storage *rs = krb5_storage_from_mem(buf, sizeof(rodc_id));
        if (rs == NULL)
            ret = krb5_enomem(context);
         else
             krb5_storage_set_flags(rs, KRB5_STORAGE_BYTEORDER_LE);
         if (ret == 0)
             ret = krb5_store_uint16(rs, rodc_id);
-        if (ret == 0)
-            ret = krb5_storage_to_data(rs, &rd);
        krb5_storage_free(rs);
-       if (ret)
-           goto out;
-       heim_assert(rd.length == sizeof(rodc_id), "invalid length");
-       memcpy((char *)d.data + priv_offset + priv_size, rd.data, rd.length);
-       krb5_data_free(&rd);
     }
 
     if (ret)
@@ -1975,6 +2086,7 @@ _krb5_kdc_pac_sign_ticket(krb5_context context,
                          krb5_const_principal upn,
                          krb5_const_principal canon_name,
                          krb5_boolean add_ticket_sig,
+                         krb5_boolean add_full_sig,
                          EncTicketPart *tkt,
                          uint64_t *pac_attributes) /* optional */
 {
@@ -2012,6 +2124,7 @@ _krb5_kdc_pac_sign_ticket(krb5_context context,
 
     ret = _krb5_pac_sign(context, pac, tkt->authtime, client, server_key,
                         kdc_key, rodc_id, upn, canon_name,
+                        add_full_sig,
                         pac_attributes, &rspac);
     if (ret == 0)
         ret = _kdc_tkt_insert_pac(context, tkt, &rspac);
index 70da1cb62665feeb43ee360ccf5c440f6e6dceda..48e872a40efc6f2c1758f34001800e4911dcb33c 100644 (file)
@@ -877,8 +877,12 @@ check_ticket_signature(krb5_context context,
 
     heim_assert(!is_krbtgt(&ticket.sname) == !!signedticket, "ticket-signature");
 
+    /*
+     * We have to not verify the KDC checksum as the saved PAC has no
+     * full checksum, and krb5_pac_verify requires this now
+     */
     ret = krb5_pac_verify(context, pac, et.authtime, client,
-                         tkt->key, tkt->kdc_key);
+                         tkt->key, NULL);
     if (ret)
        t_err(context, tkt->name, "krb5_pac_verify ticket-sig", ret);
 
@@ -899,9 +903,13 @@ check_ticket_signature(krb5_context context,
     if (ret)
        t_err(context, tkt->name, "remove_AuthorizationData", ret);
 
+    /*
+     * While we should do a full PAC signature in the same case as
+     * signedticket, the saved examples do not have one
+     */
     ret = _krb5_kdc_pac_sign_ticket(context, pac, client, tkt->key,
                                    tkt->kdc_key, tkt->rodc_id,
-                                   NULL, NULL, signedticket, &et, NULL);
+                                   NULL, NULL, signedticket, FALSE, &et, NULL);
     if (ret)
        t_err(context, tkt->name, "_krb5_kdc_pac_sign_ticket", ret);
 
@@ -920,9 +928,14 @@ check_ticket_signature(krb5_context context,
     if (ret)
        t_err(context, tkt->name, "remove_AuthorizationData 2", ret);
 
+    /*
+     * This time we will not be doing a krb5_data_cmp() so we add the
+     * full signature so that we can run that check in
+     * krb5_pac_verify()
+     */
     ret = _krb5_kdc_pac_sign_ticket(context, pac, client, tkt->key,
                                    tkt->kdc_key, tkt->rodc_id,
-                                   NULL, NULL, signedticket, &et, NULL);
+                                   NULL, NULL, signedticket, TRUE, &et, NULL);
     if (ret)
        t_err(context, tkt->name, "_krb5_kdcsignedticketsign_ticket 2", ret);
 
@@ -1018,13 +1031,18 @@ main(int argc, char **argv)
     if (ret)
        krb5_err(context, 1, ret, "krb5_pac_parse");
 
+    /*
+     * We have to not verify the KDC checksum as the saved PAC has no
+     * full checksum, and krb5_pac_verify requires this now
+     */
     ret = krb5_pac_verify(context, pac, authtime, p,
-                          &member_keyblock, &kdc_keyblock);
+                         &member_keyblock, NULL);
     if (ret)
        krb5_err(context, 1, ret, "krb5_pac_verify");
 
     ret = _krb5_pac_sign(context, pac, authtime, p,
                         &member_keyblock, &kdc_keyblock, 0, NULL, NULL,
+                        TRUE,
                         NULL, &data);
     if (ret)
        krb5_err(context, 1, ret, "_krb5_pac_sign");
@@ -1051,14 +1069,14 @@ main(int argc, char **argv)
        if (ret)
            krb5_err(context, 1, ret, "krb5_pac_init");
 
-       /* our two user buffer plus the three "system" buffers */
+       /* our two user buffer plus the four "system" buffers */
        ret = krb5_pac_get_types(context, pac, &len, &list);
        if (ret)
            krb5_err(context, 1, ret, "krb5_pac_get_types");
 
        for (i = 0; i < len; i++) {
            /* skip server_cksum, privsvr_cksum, and logon_name */
-           if (list[i] == 6 || list[i] == 7 || list[i] == 10)
+           if (list[i] == 6 || list[i] == 7 || list[i] == 10 || list[i] == 19)
                continue;
 
            ret = krb5_pac_get_buffer(context, pac, list[i], &data);
@@ -1082,7 +1100,7 @@ main(int argc, char **argv)
 
        ret = _krb5_pac_sign(context, pac2, authtime, p,
                             &member_keyblock, &kdc_keyblock, 0,
-                            NULL, NULL, NULL, &data);
+                            NULL, NULL, TRUE, NULL, &data);
        if (ret)
            krb5_err(context, 1, ret, "_krb5_pac_sign 4");
 
@@ -1182,7 +1200,7 @@ main(int argc, char **argv)
 
     ret = _krb5_pac_sign(context, pac, authtime, p,
                         &member_keyblock, &kdc_keyblock, 0,
-                        NULL, NULL, NULL, &data);
+                        NULL, NULL, TRUE, NULL, &data);
     if (ret)
        krb5_err(context, 1, ret, "_krb5_pac_sign");
 
@@ -1202,11 +1220,11 @@ main(int argc, char **argv)
        uint32_t *list;
        size_t len;
 
-       /* our two user buffer plus the three "system" buffers */
+       /* our two user buffer plus the four "system" buffers */
        ret = krb5_pac_get_types(context, pac, &len, &list);
        if (ret)
            krb5_err(context, 1, ret, "krb5_pac_get_types");
-       if (len != 5)
+       if (len != 6)
            krb5_errx(context, 1, "list wrong length");
        free(list);
     }