Merge pull request #336 from deweerdt/rfc7250

Raw public keys (RFC 7250) support
diff --git a/include/picotls.h b/include/picotls.h
index 0162fe8..ee3b941 100644
--- a/include/picotls.h
+++ b/include/picotls.h
@@ -153,6 +153,7 @@
 #define PTLS_ALERT_BAD_RECORD_MAC 20
 #define PTLS_ALERT_HANDSHAKE_FAILURE 40
 #define PTLS_ALERT_BAD_CERTIFICATE 42
+#define PTLS_ALERT_UNSUPPORTED_CERTIFICATE 43
 #define PTLS_ALERT_CERTIFICATE_REVOKED 44
 #define PTLS_ALERT_CERTIFICATE_EXPIRED 45
 #define PTLS_ALERT_CERTIFICATE_UNKNOWN 46
@@ -210,6 +211,9 @@
 #define PTLS_HANDSHAKE_TYPE_COMPRESSED_CERTIFICATE 25
 #define PTLS_HANDSHAKE_TYPE_MESSAGE_HASH 254
 
+#define PTLS_CERTIFICATE_TYPE_X509 0
+#define PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY 2
+
 #define PTLS_ZERO_DIGEST_SHA256                                                                                                    \
     {                                                                                                                              \
         0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4,    \
@@ -531,6 +535,10 @@
         const uint16_t *list;
         size_t count;
     } cipher_suites;
+    struct {
+        const uint8_t *list;
+        size_t count;
+    } server_certificate_types;
     /**
      * if ESNI was used
      */
@@ -708,6 +716,19 @@
      */
     unsigned omit_end_of_early_data : 1;
     /**
+     * This option turns on support for Raw Public Keys (RFC 7250).
+     *
+     * When running as a client, this option instructs the client to request the server to send raw public keys in place of X.509
+     * certificate chain. The client should set its `certificate_verify` callback to one that is capable of validating the raw
+     * public key that will be sent by the server.
+     *
+     * When running as a server, this option instructs the server to only handle clients requesting the use of raw public keys. If
+     * the client does not, the handshake is rejected. Note however that the rejection happens only after the `on_client_hello`
+     * callback is being called. Therefore, applications can support both X.509 and raw public keys by swapping `ptls_context_t` to
+     * the correct one when that callback is being called (like handling swapping the contexts based on the value of SNI).
+     */
+    unsigned use_raw_public_keys : 1;
+    /**
      *
      */
     ptls_encrypt_ticket_t *encrypt_ticket;
diff --git a/include/picotls/openssl.h b/include/picotls/openssl.h
index 2bfe1b9..2cc8c96 100644
--- a/include/picotls/openssl.h
+++ b/include/picotls/openssl.h
@@ -104,6 +104,11 @@
 void ptls_openssl_dispose_sign_certificate(ptls_openssl_sign_certificate_t *self);
 int ptls_openssl_load_certificates(ptls_context_t *ctx, X509 *cert, STACK_OF(X509) * chain);
 
+typedef struct st_ptls_openssl_raw_pubkey_verify_certificate_t {
+    ptls_verify_certificate_t super;
+    EVP_PKEY *expected_pubkey;
+} ptls_openssl_raw_pubkey_verify_certificate_t;
+
 typedef struct st_ptls_openssl_verify_certificate_t {
     ptls_verify_certificate_t super;
     X509_STORE *cert_store;
@@ -113,6 +118,9 @@
 void ptls_openssl_dispose_verify_certificate(ptls_openssl_verify_certificate_t *self);
 X509_STORE *ptls_openssl_create_default_certificate_store(void);
 
+int ptls_openssl_raw_pubkey_init_verify_certificate(ptls_openssl_raw_pubkey_verify_certificate_t *self, EVP_PKEY *pubkey);
+void ptls_openssl_raw_pubkey_dispose_verify_certificate(ptls_openssl_raw_pubkey_verify_certificate_t *self);
+
 int ptls_openssl_encrypt_ticket(ptls_buffer_t *dst, ptls_iovec_t src,
                                 int (*cb)(unsigned char *, unsigned char *, EVP_CIPHER_CTX *, HMAC_CTX *, int));
 int ptls_openssl_decrypt_ticket(ptls_buffer_t *dst, ptls_iovec_t src,
diff --git a/lib/openssl.c b/lib/openssl.c
index 00c7777..3221a7e 100644
--- a/lib/openssl.c
+++ b/lib/openssl.c
@@ -1286,6 +1286,51 @@
     return NULL;
 }
 
+static int verify_raw_cert(ptls_verify_certificate_t *_self, ptls_t *tls, int (**verifier)(void *, ptls_iovec_t, ptls_iovec_t),
+                           void **verify_data, ptls_iovec_t *certs, size_t num_certs)
+{
+    ptls_openssl_raw_pubkey_verify_certificate_t *self = (ptls_openssl_raw_pubkey_verify_certificate_t *)_self;
+    int ret = PTLS_ALERT_BAD_CERTIFICATE;
+    ptls_iovec_t expected_pubkey = {};
+
+    assert(num_certs != 0);
+
+    if (num_certs != 1)
+        goto Exit;
+
+    int r = i2d_PUBKEY(self->expected_pubkey, &expected_pubkey.base);
+    if (r <= 0) {
+        ret = PTLS_ALERT_BAD_CERTIFICATE;
+        goto Exit;
+    }
+
+    expected_pubkey.len = r;
+    if (certs[0].len != expected_pubkey.len)
+        goto Exit;
+
+    if (!ptls_mem_equal(expected_pubkey.base, certs[0].base, certs[0].len))
+        goto Exit;
+
+    EVP_PKEY_up_ref(self->expected_pubkey);
+    *verify_data = self->expected_pubkey;
+    *verifier = verify_sign;
+    ret = 0;
+Exit:
+    free(expected_pubkey.base);
+    return ret;
+}
+
+int ptls_openssl_raw_pubkey_init_verify_certificate(ptls_openssl_raw_pubkey_verify_certificate_t *self, EVP_PKEY *expected_pubkey)
+{
+    EVP_PKEY_up_ref(expected_pubkey);
+    *self = (ptls_openssl_raw_pubkey_verify_certificate_t){{verify_raw_cert}, expected_pubkey};
+    return 0;
+}
+void ptls_openssl_raw_pubkey_dispose_verify_certificate(ptls_openssl_raw_pubkey_verify_certificate_t *self)
+{
+    EVP_PKEY_free(self->expected_pubkey);
+}
+
 #define TICKET_LABEL_SIZE 16
 #define TICKET_IV_SIZE EVP_MAX_IV_LENGTH
 
diff --git a/lib/picotls.c b/lib/picotls.c
index 9f84120..9f405e2 100644
--- a/lib/picotls.c
+++ b/lib/picotls.c
@@ -56,6 +56,7 @@
 #define PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS 10
 #define PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS 13
 #define PTLS_EXTENSION_TYPE_ALPN 16
+#define PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE 20
 #define PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE 27
 #define PTLS_EXTENSION_TYPE_PRE_SHARED_KEY 41
 #define PTLS_EXTENSION_TYPE_EARLY_DATA 42
@@ -284,6 +285,7 @@
 
 #define MAX_UNKNOWN_EXTENSIONS 16
 #define MAX_CLIENT_CIPHERS 32
+#define MAX_CERTIFICATE_TYPES 8
 
 struct st_ptls_client_hello_t {
     uint16_t legacy_version;
@@ -335,6 +337,10 @@
         unsigned early_data_indication : 1;
         unsigned is_last_extension : 1;
     } psk;
+    struct {
+        uint8_t list[MAX_CERTIFICATE_TYPES];
+        size_t count;
+    } server_certificate_types;
     ptls_raw_extension_t unknown_extensions[MAX_UNKNOWN_EXTENSIONS + 1];
     unsigned status_request : 1;
 };
@@ -462,6 +468,10 @@
         ALLOW(CLIENT_HELLO);
         ALLOW(SERVER_HELLO);
     });
+    EXT(SERVER_CERTIFICATE_TYPE, {
+        ALLOW(CLIENT_HELLO);
+        ALLOW(ENCRYPTED_EXTENSIONS);
+    });
 
 #undef ALLOW
 #undef EXT
@@ -2064,6 +2074,11 @@
                     ptls_buffer_push_block(sendbuf, 2, { ptls_buffer_pushv(sendbuf, cookie->base, cookie->len); });
                 });
             }
+            if (tls->ctx->use_raw_public_keys) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE, {
+                    ptls_buffer_push_block(sendbuf, 1, { ptls_buffer_push(sendbuf, PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY); });
+                });
+            }
             if ((ret = push_additional_extensions(properties, sendbuf)) != 0)
                 goto Exit;
             if (tls->ctx->save_ticket != NULL || resumption_secret.base != NULL) {
@@ -2424,6 +2439,7 @@
     static const ptls_raw_extension_t no_unknown_extensions = {UINT16_MAX};
     ptls_raw_extension_t *unknown_extensions = (ptls_raw_extension_t *)&no_unknown_extensions;
     int ret, skip_early_data = 1;
+    uint8_t server_offered_cert_type = PTLS_CERTIFICATE_TYPE_X509;
 
     decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, &type, {
         if (tls->ctx->on_extension != NULL &&
@@ -2478,6 +2494,14 @@
             }
             skip_early_data = 0;
             break;
+        case PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE:
+            if (end - src != 1) {
+                ret = PTLS_ALERT_DECODE_ERROR;
+                goto Exit;
+            }
+            server_offered_cert_type = *src;
+            src = end;
+            break;
         default:
             if (should_collect_unknown_extension(tls, properties, type)) {
                 if (unknown_extensions == &no_unknown_extensions) {
@@ -2495,6 +2519,12 @@
         src = end;
     });
 
+    if (server_offered_cert_type !=
+        (tls->ctx->use_raw_public_keys ? PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY : PTLS_CERTIFICATE_TYPE_X509)) {
+        ret = PTLS_ALERT_UNSUPPORTED_CERTIFICATE;
+        goto Exit;
+    }
+
     if (tls->esni != NULL) {
         if (esni_nonce == NULL || !ptls_mem_equal(esni_nonce, tls->esni->nonce, PTLS_ESNI_NONCE_SIZE)) {
             ret = PTLS_ALERT_ILLEGAL_PARAMETER;
@@ -3285,6 +3315,23 @@
                 } while (src != end);
             });
             break;
+        case PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE:
+            ptls_decode_block(src, end, 1, {
+                size_t list_size = end - src;
+
+                /* RFC7250 4.1: No empty list, no list with single x509 element */
+                if (list_size == 0 || (list_size == 1 && *src == PTLS_CERTIFICATE_TYPE_X509)) {
+                    ret = PTLS_ALERT_DECODE_ERROR;
+                    goto Exit;
+                }
+
+                do {
+                    if (ch->server_certificate_types.count < PTLS_ELEMENTSOF(ch->server_certificate_types.list))
+                        ch->server_certificate_types.list[ch->server_certificate_types.count++] = *src;
+                    src++;
+                } while (src != end);
+            });
+            break;
         case PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE:
             ptls_decode_block(src, end, 1, {
                 do {
@@ -3583,6 +3630,18 @@
     return 0;
 }
 
+static int certificate_type_exists(uint8_t *list, size_t count, uint8_t desired_type)
+{
+    /* empty type list means that we default to x509 */
+    if (desired_type == PTLS_CERTIFICATE_TYPE_X509 && count == 0)
+        return 1;
+    for (size_t i = 0; i < count; i++) {
+        if (list[i] == desired_type)
+            return 1;
+    }
+    return 0;
+}
+
 static int server_handle_hello(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message,
                                ptls_handshake_properties_t *properties)
 {
@@ -3634,8 +3693,9 @@
         ret = PTLS_ERROR_NO_MEMORY;
         goto Exit;
     }
-    *ch = (struct st_ptls_client_hello_t){0,      NULL,   {NULL},     {NULL}, 0,     {NULL},   {NULL}, {NULL},        {{0}},
-                                          {NULL}, {NULL}, {{{NULL}}}, {{0}},  {{0}}, {{NULL}}, {NULL}, {{UINT16_MAX}}};
+
+    *ch = (struct st_ptls_client_hello_t){0,      NULL,   {NULL},     {NULL}, 0,     {NULL},   {NULL}, {NULL}, {{0}},
+                                          {NULL}, {NULL}, {{{NULL}}}, {{0}},  {{0}}, {{NULL}}, {NULL}, {{0}},  {{UINT16_MAX}}};
 
     /* decode ClientHello */
     if ((ret = decode_client_hello(tls, ch, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, properties)) !=
@@ -3721,15 +3781,24 @@
                                                         {ch->signature_algorithms.list, ch->signature_algorithms.count},
                                                         {ch->cert_compression_algos.list, ch->cert_compression_algos.count},
                                                         {ch->client_ciphers.list, ch->client_ciphers.count},
+                                                        {ch->server_certificate_types.list, ch->server_certificate_types.count},
                                                         is_esni};
             ret = tls->ctx->on_client_hello->cb(tls->ctx->on_client_hello, tls, &params);
         } else {
             ret = 0;
         }
+
         if (is_esni)
             free(server_name.base);
         if (ret != 0)
             goto Exit;
+
+        if (!certificate_type_exists(ch->server_certificate_types.list, ch->server_certificate_types.count,
+                                     tls->ctx->use_raw_public_keys ? PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY
+                                                                   : PTLS_CERTIFICATE_TYPE_X509)) {
+            ret = PTLS_ALERT_UNSUPPORTED_CERTIFICATE;
+            goto Exit;
+        }
     } else {
         if (ch->psk.early_data_indication) {
             ret = PTLS_ALERT_DECODE_ERROR;
@@ -3993,6 +4062,10 @@
                  * The "extension_data" field of this extension SHALL be empty. (RFC 6066 section 3) */
                 buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_NAME, {});
             }
+            if (tls->ctx->use_raw_public_keys) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE,
+                                      { ptls_buffer_push(sendbuf, PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY); });
+            }
             if (tls->negotiated_protocol != NULL) {
                 buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ALPN, {
                     ptls_buffer_push_block(sendbuf, 2, {
diff --git a/t/assets/ec256-key-pair.pem b/t/assets/ec256-key-pair.pem
new file mode 100644
index 0000000..ce786ed
--- /dev/null
+++ b/t/assets/ec256-key-pair.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIBswGBU5+SsrFYQupnJ/GVf1bYhBEmJiBpxLL4jXZRrpoAoGCCqGSM49
+AwEHoUQDQgAEWF0BvlHl/ZVaoApefcN5+emI6cjSDbR3aP843VWgMLfxNqvmWut0
+KsoRQC2OHJ+Z8HoLZcNnA7Mc3/ypHSUqrw==
+-----END EC PRIVATE KEY-----
diff --git a/t/assets/ec256-pub.pem b/t/assets/ec256-pub.pem
new file mode 100644
index 0000000..76a3396
--- /dev/null
+++ b/t/assets/ec256-pub.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWF0BvlHl/ZVaoApefcN5+emI6cjS
+DbR3aP843VWgMLfxNqvmWut0KsoRQC2OHJ+Z8HoLZcNnA7Mc3/ypHSUqrw==
+-----END PUBLIC KEY-----
diff --git a/t/assets/server.pub b/t/assets/server.pub
new file mode 100644
index 0000000..52b7731
--- /dev/null
+++ b/t/assets/server.pub
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7zZheZ4ph98JaedBNv9k
+qsVA9CSmhd69kBc9ZAfVFMA4VQwprOj3ZGrxf20HB3FkvqGvew9ZogUF6NjbPume
+iUObGpP21Y5wcYlPL4aojlrwMB/eOxOCpuRyQTRSSe1hDPvdJABQdmshDP5ZSEBL
+dUSgrNn4KWhIDjFj1AHXIMqeqTXetFuRgNzHdtbXQx+UWBis2B6qZJuqSArb2msV
+OC8D5gNznPPlQw7FbdPCaLNXSb6GnI0E0uj6QmYlAw9s6nkgP/zxjfFldqPNUprG
+cEqTwmAb8VVtd7XbANYrzubZ4Nn6/WXrCrVxWUmh/7Spgdwa/I4Nr1JHv9HHyL2z
+/wIDAQAB
+-----END PUBLIC KEY-----
diff --git a/t/cli.c b/t/cli.c
index bce9a65..b97c960 100644
--- a/t/cli.c
+++ b/t/cli.c
@@ -97,6 +97,7 @@
             goto Exit;
         }
     }
+
     if (server_name != NULL) {
         ptls_set_server_name(tls, server_name, 0);
         if ((ret = ptls_handshake(tls, &encbuf, NULL, NULL, hsprop)) != PTLS_ERROR_IN_PROGRESS) {
@@ -358,6 +359,11 @@
            "  -E esni-file         file that stores ESNI data generated by picotls-esni\n"
            "  -e                   when resuming a session, send first 8,192 bytes of input\n"
            "                       as early data\n"
+           "  -r                   Load raw public keys for raw certificates\n"
+           "  -r public-key-file   use raw public keys (RFC 7250). When set and running as a\n"
+           "                       client, the argument specifies the public keys that the\n"
+           "                       server is expected to use. When running as a server, the\n"
+           "                       argument is ignored.\n"
            "  -u                   update the traffic key when handshake is complete\n"
            "  -v                   verify peer using the default certificates\n"
            "  -y cipher-suite      cipher-suite to be used, e.g., aes128gcmsha256 (default:\n"
@@ -404,8 +410,9 @@
     struct sockaddr_storage sa;
     socklen_t salen;
     int family = 0;
+    const char *raw_pub_key_file = NULL, *cert_location = NULL;
 
-    while ((ch = getopt(argc, argv, "46abBC:c:i:Ik:nN:es:SE:K:l:y:vh")) != -1) {
+    while ((ch = getopt(argc, argv, "46abBC:c:i:Ik:nN:es:Sr:E:K:l:y:vh")) != -1) {
         switch (ch) {
         case '4':
             family = AF_INET;
@@ -429,11 +436,11 @@
             break;
         case 'C':
         case 'c':
-            if (ctx.certificates.count != 0) {
+            if (cert_location != NULL) {
                 fprintf(stderr, "-C/-c can only be specified once\n");
                 return 1;
             }
-            load_certificate_chain(&ctx, optarg);
+            cert_location = optarg;
             is_server = ch == 'c';
             break;
         case 'i':
@@ -451,6 +458,9 @@
         case 'e':
             use_early_data = 1;
             break;
+        case 'r':
+            raw_pub_key_file = optarg;
+            break;
         case 's':
             setup_session_file(&ctx, &hsprop, optarg);
             break;
@@ -540,10 +550,36 @@
     }
     argc -= optind;
     argv += optind;
+
+    if (raw_pub_key_file != NULL) {
+        int is_dash = !strcmp(raw_pub_key_file, "-");
+        if (is_server) {
+            ctx.certificates.list = malloc(sizeof(*ctx.certificates.list));
+            load_raw_public_key(ctx.certificates.list, cert_location);
+            ctx.certificates.count = 1;
+        } else if (!is_dash) {
+            ptls_iovec_t raw_pub_key;
+            EVP_PKEY *pubkey;
+            load_raw_public_key(&raw_pub_key, raw_pub_key_file);
+            pubkey = d2i_PUBKEY(NULL, (const unsigned char **)&raw_pub_key.base, raw_pub_key.len);
+            if (pubkey == NULL) {
+                fprintf(stderr, "Failed to create an EVP_PKEY from the key found in %s\n", raw_pub_key_file);
+                return 1;
+            }
+            setup_raw_pubkey_verify_certificate(&ctx, pubkey);
+            EVP_PKEY_free(pubkey);
+        }
+        ctx.use_raw_public_keys = 1;
+    } else {
+        if (cert_location)
+            load_certificate_chain(&ctx, cert_location);
+    }
+
     if ((ctx.certificates.count == 0) != (ctx.sign_certificate == NULL)) {
         fprintf(stderr, "-C/-c and -k options must be used together\n");
         return 1;
     }
+
     if (is_server) {
         if (ctx.certificates.count == 0) {
             fprintf(stderr, "-c and -k options must be set\n");
diff --git a/t/e2e.t b/t/e2e.t
index 577bc20..e6a4ff9 100755
--- a/t/e2e.t
+++ b/t/e2e.t
@@ -16,7 +16,7 @@
 my $tempdir = tempdir(CLEANUP => 1);
 
 subtest "hello" => sub {
-    my $guard = spawn_server(qw(-i t/assets/hello.txt));
+    my $guard = spawn_server("rsa", qw(-i t/assets/hello.txt));
     subtest "full-handshake" => sub {
         my $resp = `$cli 127.0.0.1 $port 2> /dev/null`;
         is $resp, "hello";
@@ -35,7 +35,7 @@
     subtest "success" => sub {
         plan skip_all => "faketime not found"
             unless system("which faketime > /dev/null 2>&1") == 0;
-        my $guard = spawn_server(qw(-i t/assets/hello.txt -l), "$tempdir/events");
+        my $guard = spawn_server("rsa", qw(-i t/assets/hello.txt -l), "$tempdir/events");
         my $resp = `$cli -s $tempdir/session 127.0.0.1 $port`;
         is $resp, "hello";
         $resp = `$cli -e -s $tempdir/session 127.0.0.1 $port`;
@@ -63,17 +63,41 @@
 subtest "certificate-compression" => sub {
     plan skip_all => "feature disabled"
         unless system("$cli -b -h > /dev/null 2>&1") == 0;
-    my $guard = spawn_server(qw(-i t/assets/hello.txt -b));
+    my $guard = spawn_server("rsa", qw(-i t/assets/hello.txt -b));
     my $resp = `$cli 127.0.0.1 $port 2> /dev/null`;
     is $resp, "hello";
     $resp = `$cli -b 127.0.0.1 $port 2> /dev/null`;
     is $resp, "hello";
 };
 
+subtest "raw-certificates-rsa" => sub {
+    my $guard = spawn_server("rsa", qw(-r - -i t/assets/hello.txt));
+    my $resp = `$cli -v -r t/assets/server.pub 127.0.0.1 $port 2> /dev/null`;
+    is $resp, "hello";
+};
+
+
+subtest "raw-certificates-ec" => sub {
+    my $guard = spawn_server("ec", qw(-r - -i t/assets/hello.txt));
+    my $resp = `$cli -v -r t/assets/ec256-pub.pem 127.0.0.1 $port 2> /dev/null`;
+    is $resp, "hello";
+};
+
+
 done_testing;
 
 sub spawn_server {
-    my @cmd = ($cli, "-k", "t/assets/server.key", "-c", "t/assets/server.crt", @_, "127.0.0.1", $port);
+    my $key_type = shift;
+    my @cmd;
+    if ($key_type eq "rsa") {
+        my $ext = "crt";
+        $ext = "pub" if (grep(/^-r$/, @_));
+        @cmd = ($cli, "-k", "t/assets/server.key", "-c", "t/assets/server.$ext", @_, "127.0.0.1", $port);
+    } elsif ($key_type eq "ec") {
+        @cmd = ($cli, "-k", "t/assets/ec256-key-pair.pem", "-c", "t/assets/ec256-pub.pem", @_, "127.0.0.1", $port);
+    } else {
+        die "Unexpected key type: $key_type";
+    }
     my $pid = fork;
     die "fork failed:$!"
         unless defined $pid;
diff --git a/t/util.h b/t/util.h
index db86416..3890737 100644
--- a/t/util.h
+++ b/t/util.h
@@ -48,6 +48,15 @@
     }
 }
 
+static inline void load_raw_public_key(ptls_iovec_t *raw_public_key, char const *cert_pem_file)
+{
+    size_t count;
+    if (ptls_load_pem_objects(cert_pem_file, "PUBLIC KEY", raw_public_key, 1, &count) != 0) {
+        fprintf(stderr, "failed to load public key:%s:%s\n", cert_pem_file, strerror(errno));
+        exit(1);
+    }
+}
+
 static inline void load_private_key(ptls_context_t *ctx, const char *fn)
 {
     static ptls_openssl_sign_certificate_t sc;
@@ -122,6 +131,13 @@
     ctx->verify_certificate = &vc.super;
 }
 
+static inline void setup_raw_pubkey_verify_certificate(ptls_context_t *ctx, EVP_PKEY *pubkey)
+{
+    static ptls_openssl_raw_pubkey_verify_certificate_t vc;
+    ptls_openssl_raw_pubkey_init_verify_certificate(&vc, pubkey);
+    ctx->verify_certificate = &vc.super;
+}
+
 static inline void setup_esni(ptls_context_t *ctx, const char *esni_fn, ptls_key_exchange_context_t **key_exchanges)
 {
     uint8_t esnikeys[65536];