diff --git a/kdc/pkinit-ec.c b/kdc/pkinit-ec.c
index c718aa799..31a5fe7de 100644
--- a/kdc/pkinit-ec.c
+++ b/kdc/pkinit-ec.c
@@ -52,10 +52,16 @@
  */
 
 #ifdef HAVE_HCRYPTO_W_OPENSSL
-#include <openssl/ec.h>
-#include <openssl/ecdh.h>
 #include <openssl/evp.h>
+#include <openssl/ec.h>
+#include <openssl/ecdsa.h>
+#include <openssl/rsa.h>
 #include <openssl/bn.h>
+#include <openssl/dh.h>
+#include <openssl/objects.h>
+#ifdef HAVE_OPENSSL_30
+#include <openssl/core_names.h>
+#endif
 #define HEIM_NO_CRYPTO_HDRS
 #endif /* HAVE_HCRYPTO_W_OPENSSL */
 
@@ -69,37 +75,101 @@
 #include <pkinit_asn1.h>
 
 #include <hx509.h>
-
-#ifdef HAVE_HCRYPTO_W_OPENSSL
-static void
-free_client_ec_param(krb5_context context,
-                     EC_KEY *ec_key_pk,
-                     EC_KEY *ec_key_key)
-{
-    if (ec_key_pk != NULL)
-        EC_KEY_free(ec_key_pk);
-    if (ec_key_key != NULL)
-        EC_KEY_free(ec_key_key);
-}
-#endif
+#include "../lib/hx509/hx_locl.h"
+#include <hx509-private.h>
 
 void
 _kdc_pk_free_client_ec_param(krb5_context context,
-                             void *ec_key_pk,
-                             void *ec_key_key)
+                             void *k0,
+                             void *k1)
 {
 #ifdef HAVE_HCRYPTO_W_OPENSSL
-    free_client_ec_param(context, ec_key_pk, ec_key_key);
+#ifdef HAVE_OPENSSL_30
+    EVP_PKEY_free(k0);
+    EVP_PKEY_free(k1);
+#else
+    EC_KEY_free(k0);
+    EC_KEY_free(k1);
+#endif
 #endif
 }
 
 #ifdef HAVE_HCRYPTO_W_OPENSSL
+#ifdef HAVE_OPENSSL_30
 static krb5_error_code
-generate_ecdh_keyblock(krb5_context context,
-                       EC_KEY *ec_key_pk,    /* the client's public key */
-                       EC_KEY **ec_key_key,  /* the KDC's ephemeral private */
-                       unsigned char **dh_gen_key, /* shared secret */
-                       size_t *dh_gen_keylen)
+generate_ecdh_keyblock_ossl30(krb5_context context,
+                              EVP_PKEY *ec_key_pub,   /* the client's public key */
+                              EVP_PKEY **ec_key_priv, /* the KDC's ephemeral private */
+                              unsigned char **dh_gen_key, /* shared secret */
+                              size_t *dh_gen_keylen)
+{
+    EVP_PKEY_CTX *pctx = NULL;
+    EVP_PKEY *ephemeral = NULL;
+    krb5_error_code ret = 0;
+    unsigned char *p = NULL;
+    size_t size = 0;
+
+    if (ec_key_pub == NULL)
+        /* XXX This seems like an internal error that should be impossible */
+        krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC,
+                               "Missing client ECDH key agreement public key");
+    if (ret == 0 &&
+        (ephemeral =
+             EVP_EC_gen(OSSL_EC_curve_nid2name(NID_X9_62_prime256v1))) == NULL)
+        krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC,
+                               "Could not generate an ECDH key agreement private key");
+    if (ret == 0 &&
+        (pctx = EVP_PKEY_CTX_new(ephemeral, NULL)) == NULL)
+        ret = krb5_enomem(context);
+    if (ret == 0 && EVP_PKEY_derive_init(pctx) != 1)
+        ret = krb5_enomem(context);
+    if (ret == 0 &&
+        EVP_PKEY_CTX_set_ecdh_kdf_type(pctx, EVP_PKEY_ECDH_KDF_NONE) != 1)
+        krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC,
+                               "Could not generate an ECDH key agreement private key "
+                               "(EVP_PKEY_CTX_set_dh_kdf_type)");
+    if (ret == 0 &&
+        EVP_PKEY_derive_set_peer_ex(pctx, ec_key_pub, 1) != 1)
+        krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC,
+                               "Could not generate an ECDH key agreement private key "
+                               "(EVP_PKEY_derive_set_peer_ex)");
+    if (ret == 0 &&
+        (EVP_PKEY_derive(pctx, NULL, &size) != 1 || size == 0))
+        krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC,
+                               "Could not generate an ECDH key agreement private key "
+                               "(EVP_PKEY_derive)");
+    if (ret == 0 && (p = malloc(size)) == NULL)
+        ret = krb5_enomem(context);
+    if (ret == 0 &&
+        (EVP_PKEY_derive(pctx, p, &size) != 1 || size == 0))
+        krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC,
+                               "Could not generate an ECDH key agreement private key "
+                               "(EVP_PKEY_derive)");
+
+    if (ret) {
+        EVP_PKEY_free(ephemeral);
+        ephemeral = NULL;
+        free(p);
+        p = NULL;
+        size = 0;
+    }
+
+    *ec_key_priv = ephemeral;
+    *dh_gen_keylen = size;
+    *dh_gen_key = p;
+
+    EVP_PKEY_CTX_free(pctx);
+    return ret;
+}
+#else
+
+/* The empty line above is intentional to work around an mkproto bug */
+static krb5_error_code
+generate_ecdh_keyblock_ossl11(krb5_context context,
+                              EC_KEY *ec_key_pk,    /* the client's public key */
+                              EC_KEY **ec_key_key,  /* the KDC's ephemeral private */
+                              unsigned char **dh_gen_key, /* shared secret */
+                              size_t *dh_gen_keylen)
 {
     const EC_GROUP *group;
     EC_KEY *ephemeral;
@@ -136,7 +206,7 @@ generate_ecdh_keyblock(krb5_context context,
     EC_KEY_set_group(ephemeral, group);
 
     if (EC_KEY_generate_key(ephemeral) != 1) {
-	EC_KEY_free(ephemeral);
+       EC_KEY_free(ephemeral);
         return krb5_enomem(context);
     }
 
@@ -165,6 +235,7 @@ generate_ecdh_keyblock(krb5_context context,
 
     return 0;
 }
+#endif
 #endif /* HAVE_HCRYPTO_W_OPENSSL */
 
 krb5_error_code
@@ -175,20 +246,128 @@ _kdc_generate_ecdh_keyblock(krb5_context context,
                             size_t *dh_gen_keylen)
 {
 #ifdef HAVE_HCRYPTO_W_OPENSSL
-    return generate_ecdh_keyblock(context, ec_key_pk,
-                                  (EC_KEY **)ec_key_key,
-                                  dh_gen_key, dh_gen_keylen);
+#ifdef HAVE_OPENSSL_30
+    return generate_ecdh_keyblock_ossl30(context, ec_key_pk,
+                                         (EVP_PKEY **)ec_key_key,
+                                         dh_gen_key, dh_gen_keylen);
+#else
+    return generate_ecdh_keyblock_ossl11(context, ec_key_pk,
+                                         (EC_KEY **)ec_key_key,
+                                         dh_gen_key, dh_gen_keylen);
+#endif
 #else
     return ENOTSUP;
 #endif /* HAVE_HCRYPTO_W_OPENSSL */
 }
 
 #ifdef HAVE_HCRYPTO_W_OPENSSL
+#ifdef HAVE_OPENSSL_30
 static krb5_error_code
-get_ecdh_param(krb5_context context,
-               krb5_kdc_configuration *config,
-               SubjectPublicKeyInfo *dh_key_info,
-               EC_KEY **out)
+get_ecdh_param_ossl30(krb5_context context,
+                      krb5_kdc_configuration *config,
+                      SubjectPublicKeyInfo *dh_key_info,
+                      EVP_PKEY **out)
+{
+    EVP_PKEY_CTX *pctx = NULL;
+    EVP_PKEY *template = NULL;
+    EVP_PKEY *public = NULL;
+    OSSL_PARAM params[2];
+    krb5_error_code ret = 0;
+    ECParameters ecp;
+    const unsigned char *p;
+    const char *curve_sn = NULL;
+    size_t len;
+    char *curve_sn_dup = NULL;
+    int groupnid = NID_undef;
+
+    /* XXX Algorithm agility; XXX KRB5_BADMSGTYPE?? */
+
+    /*
+     * In order for d2i_PublicKey() to work we need to create a template key
+     * that has the curve parameters for the subjectPublicKey.
+     *
+     * Or maybe we could learn to use the OSSL_DECODER(3) API.  But this works,
+     * at least until OpenSSL deprecates d2i_PublicKey() and forces us to use
+     * OSSL_DECODER(3).
+     */
+
+    memset(&ecp, 0, sizeof(ecp));
+
+    if (dh_key_info->algorithm.parameters == NULL)
+	krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+			       "PKINIT missing algorithm parameter "
+			       "in clientPublicValue");
+    if (ret == 0)
+        ret = decode_ECParameters(dh_key_info->algorithm.parameters->data,
+                                  dh_key_info->algorithm.parameters->length,
+                                  &ecp, &len);
+    if (ret == 0 && ecp.element != choice_ECParameters_namedCurve)
+        krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+                               "PKINIT client used an unnamed curve");
+    if (ret == 0 &&
+        (groupnid = _hx509_ossl_oid2nid(&ecp.u.namedCurve)) == NID_undef)
+        krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+                               "PKINIT client used an unsupported curve");
+    if (ret == 0 && (curve_sn = OBJ_nid2sn(groupnid)) == NULL)
+        krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+                               "Could not resolve curve NID %d to its short name",
+                               groupnid);
+    if (ret == 0 && (curve_sn_dup = strdup(curve_sn)) == NULL)
+        ret = krb5_enomem(context);
+    if (ret == 0) {
+        if (der_heim_oid_cmp(&ecp.u.namedCurve, &asn1_oid_id_ec_group_secp256r1) != 0)
+            krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+                                   "PKINIT client used an unsupported curve");
+    }
+    if (ret == 0) {
+        /*
+         * Apparently there's no error checking to be done here?  Why does
+         * OSSL_PARAM_construct_utf8_string() want a non-const for the value?
+         * Is that a bug in OpenSSL?
+         */
+        params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME,
+                                                     curve_sn_dup, 0);
+        params[1] = OSSL_PARAM_construct_end();
+
+        if ((pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL)) == NULL)
+            ret = krb5_enomem(context);
+    }
+    if (ret == 0 && EVP_PKEY_fromdata_init(pctx) != 1)
+        ret = krb5_enomem(context);
+    if (ret == 0 &&
+        EVP_PKEY_fromdata(pctx, &template, OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS,
+                          params) != 1)
+        krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+                               "Could not set up to parse key for curve %s",
+                               curve_sn);
+
+    p = dh_key_info->subjectPublicKey.data;
+    len = dh_key_info->subjectPublicKey.length / 8;
+    if (ret == 0 &&
+        (public = d2i_PublicKey(EVP_PKEY_EC, &template, &p, len)) == NULL)
+        krb5_set_error_message(context, ret = KRB5_BADMSGTYPE,
+                               "Could not decode PKINIT client ECDH key");
+
+    if (ret) {
+        EVP_PKEY_free(public);
+        public = NULL;
+    }
+
+    *out = public;
+
+    /* FYI the EVP_PKEY_CTX takes ownership of the `template' key */
+    EVP_PKEY_CTX_free(pctx);
+    free_ECParameters(&ecp);
+    free(curve_sn_dup);
+    return ret;
+}
+#else
+
+static krb5_error_code
+get_ecdh_param_ossl11(krb5_context context,
+                      krb5_kdc_configuration *config,
+                      SubjectPublicKeyInfo *dh_key_info,
+                      EC_KEY **out)
 {
     ECParameters ecp;
     EC_KEY *public = NULL;
@@ -198,30 +377,31 @@ get_ecdh_param(krb5_context context,
     int nid;
 
     if (dh_key_info->algorithm.parameters == NULL) {
-	krb5_set_error_message(context, KRB5_BADMSGTYPE,
-			       "PKINIT missing algorithm parameter "
-			       "in clientPublicValue");
-	return KRB5_BADMSGTYPE;
+       krb5_set_error_message(context, KRB5_BADMSGTYPE,
+                              "PKINIT missing algorithm parameter "
+                              "in clientPublicValue");
+       return KRB5_BADMSGTYPE;
     }
+    /* XXX Algorithm agility; XXX KRB5_BADMSGTYPE?? */
 
     memset(&ecp, 0, sizeof(ecp));
 
     ret = decode_ECParameters(dh_key_info->algorithm.parameters->data,
-			      dh_key_info->algorithm.parameters->length, &ecp, &len);
+                             dh_key_info->algorithm.parameters->length, &ecp, &len);
     if (ret)
-	goto out;
+       goto out;
 
     if (ecp.element != choice_ECParameters_namedCurve) {
-	ret = KRB5_BADMSGTYPE;
-	goto out;
+       ret = KRB5_BADMSGTYPE;
+       goto out;
     }
 
     if (der_heim_oid_cmp(&ecp.u.namedCurve, &asn1_oid_id_ec_group_secp256r1) == 0)
-	nid = NID_X9_62_prime256v1;
+       nid = NID_X9_62_prime256v1;
     else {
-	ret = KRB5_BADMSGTYPE;
-	goto out;
-    }
+       ret = KRB5_BADMSGTYPE;
+       goto out;
+   }
 
     /* XXX verify group is ok */
 
@@ -230,20 +410,21 @@ get_ecdh_param(krb5_context context,
     p = dh_key_info->subjectPublicKey.data;
     len = dh_key_info->subjectPublicKey.length / 8;
     if (o2i_ECPublicKey(&public, &p, len) == NULL) {
-	ret = KRB5_BADMSGTYPE;
-	krb5_set_error_message(context, ret,
-			       "PKINIT failed to decode ECDH key");
-	goto out;
+       ret = KRB5_BADMSGTYPE;
+       krb5_set_error_message(context, ret,
+                              "PKINIT failed to decode ECDH key");
+       goto out;
     }
     *out = public;
     public = NULL;
 
  out:
     if (public)
-	EC_KEY_free(public);
+       EC_KEY_free(public);
     free_ECParameters(&ecp);
     return ret;
 }
+#endif
 #endif /* HAVE_HCRYPTO_W_OPENSSL */
 
 krb5_error_code
@@ -253,7 +434,11 @@ _kdc_get_ecdh_param(krb5_context context,
                     void **out)
 {
 #ifdef HAVE_HCRYPTO_W_OPENSSL
-    return get_ecdh_param(context, config, dh_key_info, (EC_KEY **)out);
+#ifdef HAVE_OPENSSL_30
+    return get_ecdh_param_ossl30(context, config, dh_key_info, (EVP_PKEY **)out);
+#else
+    return get_ecdh_param_ossl11(context, config, dh_key_info, (EC_KEY **)out);
+#endif
 #else
     return ENOTSUP;
 #endif /* HAVE_HCRYPTO_W_OPENSSL */
@@ -265,13 +450,51 @@ _kdc_get_ecdh_param(krb5_context context,
  */
 
 #ifdef HAVE_HCRYPTO_W_OPENSSL
+#ifdef HAVE_OPENSSL_30
 static krb5_error_code
-serialize_ecdh_key(krb5_context context,
-                   EC_KEY *key,
-                   unsigned char **out,
-                   size_t *out_len)
+serialize_ecdh_key_ossl30(krb5_context context,
+                          EVP_PKEY *key,
+                          unsigned char **out,
+                          size_t *out_len)
+{
+    unsigned char *p;
+    int len;
+
+    *out = NULL;
+    *out_len = 0;
+
+    len = i2d_PublicKey(key, NULL);
+    if (len <= 0) {
+        krb5_set_error_message(context, EOVERFLOW,
+                               "PKINIT failed to encode ECDH key");
+        return EOVERFLOW;
+    }
+
+    *out = malloc(len);
+    if (*out == NULL)
+        return krb5_enomem(context);
+
+    p = *out;
+    len = i2d_PublicKey(key, &p);
+    if (len <= 0) {
+        free(*out);
+        *out = NULL;
+	krb5_set_error_message(context, EINVAL /* XXX Better error please */,
+			       "PKINIT failed to encode ECDH key");
+        return EINVAL;
+    }
+
+    *out_len = len * 8;
+    return 0;
+}
+#else
+
+static krb5_error_code
+serialize_ecdh_key_ossl11(krb5_context context,
+                          EC_KEY *key,
+                          unsigned char **out,
+                          size_t *out_len)
 {
-    krb5_error_code ret = 0;
     unsigned char *p;
     int len;
 
@@ -279,8 +502,11 @@ serialize_ecdh_key(krb5_context context,
     *out_len = 0;
 
     len = i2o_ECPublicKey(key, NULL);
-    if (len <= 0)
+    if (len <= 0) {
+        krb5_set_error_message(context, EOVERFLOW,
+                               "PKINIT failed to encode ECDH key");
         return EOVERFLOW;
+    }
 
     *out = malloc(len);
     if (*out == NULL)
@@ -291,16 +517,16 @@ serialize_ecdh_key(krb5_context context,
     if (len <= 0) {
         free(*out);
         *out = NULL;
-        ret = EINVAL; /* XXX Better error please */
-	krb5_set_error_message(context, ret,
+	krb5_set_error_message(context, EINVAL /* XXX Better error please */,
 			       "PKINIT failed to encode ECDH key");
-        return ret;
+        return EINVAL;
     }
 
     *out_len = len * 8;
-    return ret;
+    return 0;
 }
 #endif
+#endif
 
 krb5_error_code
 _kdc_serialize_ecdh_key(krb5_context context,
@@ -309,7 +535,11 @@ _kdc_serialize_ecdh_key(krb5_context context,
                         size_t *out_len)
 {
 #ifdef HAVE_HCRYPTO_W_OPENSSL
-    return serialize_ecdh_key(context, key, out, out_len);
+#ifdef HAVE_OPENSSL_30
+    return serialize_ecdh_key_ossl30(context, key, out, out_len);
+#else
+    return serialize_ecdh_key_ossl11(context, key, out, out_len);
+#endif
 #else
     return ENOTSUP;
 #endif