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 at 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 974ccf7..8e22208 100644 --- a/libopenconnect.map.in +++ b/libopenconnect.map.in @@ -77,9 +77,11 @@ OPENCONNECT_5_2 { OPENCONNECT_5_3 { global: openconnect_override_getaddrinfo; + openconnect_free_peer_cert_chain; openconnect_get_cstp_compression; openconnect_get_dnsname; openconnect_get_dtls_compression; + openconnect_get_peer_cert_chain; openconnect_disable_ipv6; openconnect_set_localname; openconnect_set_reconnected_handler; 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