| From 28bea7d86e2f5f83ff02a2d92c349c8d3af6bb1c Mon Sep 17 00:00:00 2001 |
| From: Kevin Cernekee <cernekee@gmail.com> |
| Date: Sun, 10 Apr 2016 23:13:08 -0700 |
| Subject: [PATCH 7/8] library: Add openconnect_get_peer_cert_chain() |
| |
| Allow external validation of the entire certificate chain, not just the |
| peer_cert. Tested using a letsencrypt cert on Chrome OS. |
| |
| Signed-off-by: Kevin Cernekee <cernekee@gmail.com> |
| --- |
| gnutls.c | 39 ++++++++++++++- |
| java/src/com/example/LibTest.java | 2 + |
| .../infradead/libopenconnect/LibOpenConnect.java | 1 + |
| jni.c | 49 +++++++++++++++++++ |
| libopenconnect.map.in | 2 + |
| openconnect-internal.h | 2 + |
| openconnect.h | 18 +++++++ |
| openssl.c | 55 ++++++++++++++++++++-- |
| 8 files changed, 163 insertions(+), 5 deletions(-) |
| |
| diff --git a/gnutls.c b/gnutls.c |
| index 2a93dac..338f7a7 100644 |
| --- a/gnutls.c |
| +++ b/gnutls.c |
| @@ -1948,6 +1948,38 @@ void openconnect_free_cert_info(struct openconnect_info *vpninfo, |
| gnutls_free(buf); |
| } |
| |
| +int openconnect_get_peer_cert_chain(struct openconnect_info *vpninfo, |
| + struct oc_cert **chainp) |
| +{ |
| + struct oc_cert *chain, *p; |
| + const gnutls_datum_t *cert_list = vpninfo->cert_list_handle; |
| + int i, cert_list_size = vpninfo->cert_list_size; |
| + |
| + if (!cert_list) |
| + return -EINVAL; |
| + |
| + if (cert_list_size <= 0) |
| + return -EIO; |
| + |
| + p = chain = calloc(cert_list_size, sizeof(struct oc_cert)); |
| + if (!chain) |
| + return -ENOMEM; |
| + |
| + for (i = 0; i < cert_list_size; i++, p++) { |
| + p->der_data = (unsigned char *)cert_list[i].data; |
| + p->der_len = cert_list[i].size; |
| + } |
| + |
| + *chainp = chain; |
| + return cert_list_size; |
| +} |
| + |
| +void openconnect_free_peer_cert_chain(struct openconnect_info *vpninfo, |
| + struct oc_cert *chain) |
| +{ |
| + free(chain); |
| +} |
| + |
| static int verify_peer(gnutls_session_t session) |
| { |
| struct openconnect_info *vpninfo = gnutls_session_get_ptr(session); |
| @@ -2079,10 +2111,13 @@ static int verify_peer(gnutls_session_t session) |
| vpn_progress(vpninfo, PRG_INFO, |
| _("Server certificate verify failed: %s\n"), |
| reason); |
| - if (vpninfo->validate_peer_cert) |
| + if (vpninfo->validate_peer_cert) { |
| + vpninfo->cert_list_handle = (void *)cert_list; |
| + vpninfo->cert_list_size = cert_list_size; |
| err = vpninfo->validate_peer_cert(vpninfo->cbdata, |
| reason) ? GNUTLS_E_CERTIFICATE_ERROR : 0; |
| - else |
| + vpninfo->cert_list_handle = NULL; |
| + } else |
| err = GNUTLS_E_CERTIFICATE_ERROR; |
| } |
| |
| diff --git a/java/src/com/example/LibTest.java b/java/src/com/example/LibTest.java |
| index da073b7..78e77b5 100644 |
| --- a/java/src/com/example/LibTest.java |
| +++ b/java/src/com/example/LibTest.java |
| @@ -49,6 +49,8 @@ public final class LibTest { |
| |
| byte der[] = getPeerCertDER(); |
| System.out.println("DER is " + der.length + " bytes long"); |
| + byte chain[][] = getPeerCertChain(); |
| + System.out.println("Chain has " + chain.length + " certs"); |
| |
| System.out.print("\nAccept this certificate? [n] "); |
| String s = getline(); |
| diff --git a/java/src/org/infradead/libopenconnect/LibOpenConnect.java b/java/src/org/infradead/libopenconnect/LibOpenConnect.java |
| index 3f70b2b..ce4ffcc 100644 |
| --- a/java/src/org/infradead/libopenconnect/LibOpenConnect.java |
| +++ b/java/src/org/infradead/libopenconnect/LibOpenConnect.java |
| @@ -156,6 +156,7 @@ public abstract class LibOpenConnect { |
| public synchronized native String getPeerCertHash(); |
| public synchronized native String getPeerCertDetails(); |
| public synchronized native byte[] getPeerCertDER(); |
| + public synchronized native byte[][] getPeerCertChain(); |
| |
| /* library info */ |
| |
| diff --git a/jni.c b/jni.c |
| index bfcdaa5..d72ac2e 100644 |
| --- a/jni.c |
| +++ b/jni.c |
| @@ -785,6 +785,55 @@ JNIEXPORT jbyteArray JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_ge |
| return jresult; |
| } |
| |
| +/* special handling: callee-allocated, caller-freed binary buffer */ |
| +JNIEXPORT jbyteArray JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_getPeerCertChain( |
| + JNIEnv *jenv, jobject jobj) |
| +{ |
| + struct libctx *ctx = getctx(jenv, jobj); |
| + struct oc_cert *chain = NULL, *p; |
| + int cert_list_size, i; |
| + jobjectArray jresult = NULL; |
| + jclass jcls; |
| + |
| + if (!ctx) |
| + goto err; |
| + cert_list_size = openconnect_get_peer_cert_chain(ctx->vpninfo, &chain); |
| + if (cert_list_size <= 0) |
| + goto err; |
| + |
| + jcls = (*ctx->jenv)->FindClass(ctx->jenv, "[B"); |
| + if (!jcls) |
| + goto err; |
| + |
| + jresult = (*ctx->jenv)->NewObjectArray(ctx->jenv, cert_list_size, jcls, NULL); |
| + if (!jresult) |
| + goto err; |
| + |
| + if ((*ctx->jenv)->PushLocalFrame(ctx->jenv, 256) < 0) |
| + goto err; |
| + |
| + for (i = 0, p = chain; i < cert_list_size; i++, p++) { |
| + jbyteArray cert = (*ctx->jenv)->NewByteArray(ctx->jenv, p->der_len); |
| + if (!cert) |
| + goto err2; |
| + (*ctx->jenv)->SetByteArrayRegion(ctx->jenv, cert, 0, p->der_len, (jbyte *)p->der_data); |
| + (*ctx->jenv)->SetObjectArrayElement(ctx->jenv, jresult, i, cert); |
| + } |
| + |
| + (*ctx->jenv)->PopLocalFrame(ctx->jenv, NULL); |
| + openconnect_free_peer_cert_chain(ctx->vpninfo, chain); |
| + return jresult; |
| + |
| +err2: |
| + (*ctx->jenv)->PopLocalFrame(ctx->jenv, NULL); |
| +err: |
| + if (jresult) |
| + (*ctx->jenv)->DeleteLocalRef(ctx->jenv, jresult); |
| + if (chain) |
| + openconnect_free_peer_cert_chain(ctx->vpninfo, chain); |
| + return NULL; |
| +} |
| + |
| /* special handling: two string arguments */ |
| JNIEXPORT void JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_setClientCert( |
| JNIEnv *jenv, jobject jobj, jstring jcert, jstring jsslkey) |
| diff --git a/libopenconnect.map.in b/libopenconnect.map.in |
| index deaf058..f832dda 100644 |
| --- a/libopenconnect.map.in |
| +++ b/libopenconnect.map.in |
| @@ -3,6 +3,7 @@ OPENCONNECT_5.0 { |
| openconnect_check_peer_cert_hash; |
| openconnect_clear_cookie; |
| openconnect_free_cert_info; |
| + openconnect_free_peer_cert_chain; |
| openconnect_get_cookie; |
| openconnect_get_cstp_cipher; |
| openconnect_get_dnsname; |
| @@ -11,6 +12,7 @@ OPENCONNECT_5.0 { |
| openconnect_get_ifname; |
| openconnect_get_ip_info; |
| openconnect_get_peer_cert_DER; |
| + openconnect_get_peer_cert_chain; |
| openconnect_get_peer_cert_details; |
| openconnect_get_peer_cert_hash; |
| openconnect_get_port; |
| diff --git a/openconnect-internal.h b/openconnect-internal.h |
| index b339ef6..4ded761 100644 |
| --- a/openconnect-internal.h |
| +++ b/openconnect-internal.h |
| @@ -443,6 +443,8 @@ struct openconnect_info { |
| |
| void *peer_cert; |
| char *peer_cert_hash; |
| + void *cert_list_handle; |
| + int cert_list_size; |
| |
| char *cookie; /* Pointer to within cookies list */ |
| struct oc_vpn_option *cookies; |
| diff --git a/openconnect.h b/openconnect.h |
| index d34aae0..904a92a 100644 |
| --- a/openconnect.h |
| +++ b/openconnect.h |
| @@ -46,6 +46,8 @@ extern "C" { |
| * - Add openconnect_set_setup_tun_handler(). |
| * - Add openconnect_set_reconnected_handler(). |
| * - Add openconnect_get_dnsname(). |
| + * - Add openconnect_get_peer_cert_chain() and |
| + * openconnect_free_peer_cert_chain(). |
| * |
| * API version 5.2 (v7.05; 2015-03-10): |
| * - Add openconnect_set_http_auth(), openconnect_set_protocol(). |
| @@ -273,6 +275,12 @@ struct oc_stats { |
| uint64_t rx_bytes; |
| }; |
| |
| +struct oc_cert { |
| + int der_len; |
| + unsigned char *der_data; |
| + void *reserved; |
| +}; |
| + |
| /****************************************************************************/ |
| |
| #define PRG_ERR 0 |
| @@ -367,6 +375,16 @@ int openconnect_get_peer_cert_DER(struct openconnect_info *vpninfo, |
| unsigned char **buf); |
| void openconnect_free_cert_info(struct openconnect_info *vpninfo, |
| void *buf); |
| + |
| +/* Creates a list of all certs in the peer's chain, returning the |
| + number of certs in the chain (or <0 on error). Only valid inside the |
| + validate_peer_cert callback. The caller should free the chain, |
| + but should not modify the contents. */ |
| +int openconnect_get_peer_cert_chain(struct openconnect_info *vpninfo, |
| + struct oc_cert **chain); |
| +void openconnect_free_peer_cert_chain(struct openconnect_info *vpninfo, |
| + struct oc_cert *chain); |
| + |
| /* Contains a comma-separated list of authentication methods to enabled. |
| Currently supported: Negotiate,NTLM,Digest,Basic */ |
| int openconnect_set_http_auth(struct openconnect_info *vpninfo, |
| diff --git a/openssl.c b/openssl.c |
| index 007809b..8a5ef56 100644 |
| --- a/openssl.c |
| +++ b/openssl.c |
| @@ -1325,6 +1325,48 @@ static void workaround_openssl_certchain_bug(struct openconnect_info *vpninfo, |
| X509_STORE_CTX_cleanup(&ctx); |
| } |
| |
| +int openconnect_get_peer_cert_chain(struct openconnect_info *vpninfo, |
| + struct oc_cert **chainp) |
| +{ |
| + struct oc_cert *chain, *p; |
| + X509_STORE_CTX *ctx = vpninfo->cert_list_handle; |
| + int i, cert_list_size; |
| + |
| + if (!ctx) |
| + return -EINVAL; |
| + |
| + cert_list_size = sk_X509_num(ctx->untrusted); |
| + if (!cert_list_size) |
| + return -EIO; |
| + |
| + p = chain = calloc(cert_list_size, sizeof(struct oc_cert)); |
| + if (!chain) |
| + return -ENOMEM; |
| + |
| + for (i = 0; i < cert_list_size; i++, p++) { |
| + X509 *cert = sk_X509_value(ctx->untrusted, i); |
| + |
| + p->der_len = i2d_X509(cert, &p->der_data); |
| + if (p->der_len < 0) { |
| + openconnect_free_peer_cert_chain(vpninfo, chain); |
| + return -ENOMEM; |
| + } |
| + } |
| + |
| + *chainp = chain; |
| + return cert_list_size; |
| +} |
| + |
| +void openconnect_free_peer_cert_chain(struct openconnect_info *vpninfo, |
| + struct oc_cert *chain) |
| +{ |
| + int i; |
| + |
| + for (i = 0; i < vpninfo->cert_list_size; i++) |
| + OPENSSL_free(chain[i].der_data); |
| + free(chain); |
| +} |
| + |
| static int ssl_app_verify_callback(X509_STORE_CTX *ctx, void *arg) |
| { |
| struct openconnect_info *vpninfo = arg; |
| @@ -1375,9 +1417,16 @@ static int ssl_app_verify_callback(X509_STORE_CTX *ctx, void *arg) |
| _("Server certificate verify failed: %s\n"), |
| err_string); |
| |
| - if (vpninfo->validate_peer_cert && |
| - !vpninfo->validate_peer_cert(vpninfo->cbdata, err_string)) |
| - return 1; |
| + if (vpninfo->validate_peer_cert) { |
| + int ret; |
| + |
| + vpninfo->cert_list_handle = ctx; |
| + ret = vpninfo->validate_peer_cert(vpninfo->cbdata, err_string); |
| + vpninfo->cert_list_handle = NULL; |
| + |
| + if (!ret) |
| + return 1; |
| + } |
| |
| return 0; |
| } |
| -- |
| 1.9.1 |
| |