summaryrefslogtreecommitdiff
path: root/src/libgit2/streams/stransport.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libgit2/streams/stransport.c')
-rw-r--r--src/libgit2/streams/stransport.c326
1 files changed, 326 insertions, 0 deletions
diff --git a/src/libgit2/streams/stransport.c b/src/libgit2/streams/stransport.c
new file mode 100644
index 000000000..3f31d2541
--- /dev/null
+++ b/src/libgit2/streams/stransport.c
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "streams/stransport.h"
+
+#ifdef GIT_SECURE_TRANSPORT
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <Security/SecureTransport.h>
+#include <Security/SecCertificate.h>
+
+#include "git2/transport.h"
+
+#include "streams/socket.h"
+
+static int stransport_error(OSStatus ret)
+{
+ CFStringRef message;
+
+ if (ret == noErr || ret == errSSLClosedGraceful) {
+ git_error_clear();
+ return 0;
+ }
+
+#if !TARGET_OS_IPHONE
+ message = SecCopyErrorMessageString(ret, NULL);
+ GIT_ERROR_CHECK_ALLOC(message);
+
+ git_error_set(GIT_ERROR_NET, "SecureTransport error: %s", CFStringGetCStringPtr(message, kCFStringEncodingUTF8));
+ CFRelease(message);
+#else
+ git_error_set(GIT_ERROR_NET, "SecureTransport error: OSStatus %d", (unsigned int)ret);
+ GIT_UNUSED(message);
+#endif
+
+ return -1;
+}
+
+typedef struct {
+ git_stream parent;
+ git_stream *io;
+ int owned;
+ SSLContextRef ctx;
+ CFDataRef der_data;
+ git_cert_x509 cert_info;
+} stransport_stream;
+
+static int stransport_connect(git_stream *stream)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+ int error;
+ SecTrustRef trust = NULL;
+ SecTrustResultType sec_res;
+ OSStatus ret;
+
+ if (st->owned && (error = git_stream_connect(st->io)) < 0)
+ return error;
+
+ ret = SSLHandshake(st->ctx);
+ if (ret != errSSLServerAuthCompleted) {
+ git_error_set(GIT_ERROR_SSL, "unexpected return value from ssl handshake %d", (int)ret);
+ return -1;
+ }
+
+ if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr)
+ goto on_error;
+
+ if (!trust)
+ return GIT_ECERTIFICATE;
+
+ if ((ret = SecTrustEvaluate(trust, &sec_res)) != noErr)
+ goto on_error;
+
+ CFRelease(trust);
+
+ if (sec_res == kSecTrustResultInvalid || sec_res == kSecTrustResultOtherError) {
+ git_error_set(GIT_ERROR_SSL, "internal security trust error");
+ return -1;
+ }
+
+ if (sec_res == kSecTrustResultDeny || sec_res == kSecTrustResultRecoverableTrustFailure ||
+ sec_res == kSecTrustResultFatalTrustFailure) {
+ git_error_set(GIT_ERROR_SSL, "untrusted connection error");
+ return GIT_ECERTIFICATE;
+ }
+
+ return 0;
+
+on_error:
+ if (trust)
+ CFRelease(trust);
+
+ return stransport_error(ret);
+}
+
+static int stransport_certificate(git_cert **out, git_stream *stream)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+ SecTrustRef trust = NULL;
+ SecCertificateRef sec_cert;
+ OSStatus ret;
+
+ if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr)
+ return stransport_error(ret);
+
+ sec_cert = SecTrustGetCertificateAtIndex(trust, 0);
+ st->der_data = SecCertificateCopyData(sec_cert);
+ CFRelease(trust);
+
+ if (st->der_data == NULL) {
+ git_error_set(GIT_ERROR_SSL, "retrieved invalid certificate data");
+ return -1;
+ }
+
+ st->cert_info.parent.cert_type = GIT_CERT_X509;
+ st->cert_info.data = (void *) CFDataGetBytePtr(st->der_data);
+ st->cert_info.len = CFDataGetLength(st->der_data);
+
+ *out = (git_cert *)&st->cert_info;
+ return 0;
+}
+
+static int stransport_set_proxy(
+ git_stream *stream,
+ const git_proxy_options *proxy_opts)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+
+ return git_stream_set_proxy(st->io, proxy_opts);
+}
+
+/*
+ * Contrary to typical network IO callbacks, Secure Transport write callback is
+ * expected to write *all* passed data, not just as much as it can, and any
+ * other case would be considered a failure.
+ *
+ * This behavior is actually not specified in the Apple documentation, but is
+ * required for things to work correctly (and incidentally, that's also how
+ * Apple implements it in its projects at opensource.apple.com).
+ *
+ * Libgit2 streams happen to already have this very behavior so this is just
+ * passthrough.
+ */
+static OSStatus write_cb(SSLConnectionRef conn, const void *data, size_t *len)
+{
+ git_stream *io = (git_stream *) conn;
+
+ if (git_stream__write_full(io, data, *len, 0) < 0)
+ return -36; /* "ioErr" from MacErrors.h which is not available on iOS */
+
+ return noErr;
+}
+
+static ssize_t stransport_write(git_stream *stream, const char *data, size_t len, int flags)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+ size_t data_len, processed;
+ OSStatus ret;
+
+ GIT_UNUSED(flags);
+
+ data_len = min(len, SSIZE_MAX);
+ if ((ret = SSLWrite(st->ctx, data, data_len, &processed)) != noErr)
+ return stransport_error(ret);
+
+ GIT_ASSERT(processed < SSIZE_MAX);
+ return (ssize_t)processed;
+}
+
+/*
+ * Contrary to typical network IO callbacks, Secure Transport read callback is
+ * expected to read *exactly* the requested number of bytes, not just as much
+ * as it can, and any other case would be considered a failure.
+ *
+ * This behavior is actually not specified in the Apple documentation, but is
+ * required for things to work correctly (and incidentally, that's also how
+ * Apple implements it in its projects at opensource.apple.com).
+ */
+static OSStatus read_cb(SSLConnectionRef conn, void *data, size_t *len)
+{
+ git_stream *io = (git_stream *) conn;
+ OSStatus error = noErr;
+ size_t off = 0;
+ ssize_t ret;
+
+ do {
+ ret = git_stream_read(io, data + off, *len - off);
+ if (ret < 0) {
+ error = -36; /* "ioErr" from MacErrors.h which is not available on iOS */
+ break;
+ }
+ if (ret == 0) {
+ error = errSSLClosedGraceful;
+ break;
+ }
+
+ off += ret;
+ } while (off < *len);
+
+ *len = off;
+ return error;
+}
+
+static ssize_t stransport_read(git_stream *stream, void *data, size_t len)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+ size_t processed;
+ OSStatus ret;
+
+ if ((ret = SSLRead(st->ctx, data, len, &processed)) != noErr)
+ return stransport_error(ret);
+
+ return processed;
+}
+
+static int stransport_close(git_stream *stream)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+ OSStatus ret;
+
+ ret = SSLClose(st->ctx);
+ if (ret != noErr && ret != errSSLClosedGraceful)
+ return stransport_error(ret);
+
+ return st->owned ? git_stream_close(st->io) : 0;
+}
+
+static void stransport_free(git_stream *stream)
+{
+ stransport_stream *st = (stransport_stream *) stream;
+
+ if (st->owned)
+ git_stream_free(st->io);
+
+ CFRelease(st->ctx);
+ if (st->der_data)
+ CFRelease(st->der_data);
+ git__free(st);
+}
+
+static int stransport_wrap(
+ git_stream **out,
+ git_stream *in,
+ const char *host,
+ int owned)
+{
+ stransport_stream *st;
+ OSStatus ret;
+
+ GIT_ASSERT_ARG(out);
+ GIT_ASSERT_ARG(in);
+ GIT_ASSERT_ARG(host);
+
+ st = git__calloc(1, sizeof(stransport_stream));
+ GIT_ERROR_CHECK_ALLOC(st);
+
+ st->io = in;
+ st->owned = owned;
+
+ st->ctx = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
+ if (!st->ctx) {
+ git_error_set(GIT_ERROR_NET, "failed to create SSL context");
+ git__free(st);
+ return -1;
+ }
+
+ if ((ret = SSLSetIOFuncs(st->ctx, read_cb, write_cb)) != noErr ||
+ (ret = SSLSetConnection(st->ctx, st->io)) != noErr ||
+ (ret = SSLSetSessionOption(st->ctx, kSSLSessionOptionBreakOnServerAuth, true)) != noErr ||
+ (ret = SSLSetProtocolVersionMin(st->ctx, kTLSProtocol1)) != noErr ||
+ (ret = SSLSetProtocolVersionMax(st->ctx, kTLSProtocol12)) != noErr ||
+ (ret = SSLSetPeerDomainName(st->ctx, host, strlen(host))) != noErr) {
+ CFRelease(st->ctx);
+ git__free(st);
+ return stransport_error(ret);
+ }
+
+ st->parent.version = GIT_STREAM_VERSION;
+ st->parent.encrypted = 1;
+ st->parent.proxy_support = git_stream_supports_proxy(st->io);
+ st->parent.connect = stransport_connect;
+ st->parent.certificate = stransport_certificate;
+ st->parent.set_proxy = stransport_set_proxy;
+ st->parent.read = stransport_read;
+ st->parent.write = stransport_write;
+ st->parent.close = stransport_close;
+ st->parent.free = stransport_free;
+
+ *out = (git_stream *) st;
+ return 0;
+}
+
+int git_stransport_stream_wrap(
+ git_stream **out,
+ git_stream *in,
+ const char *host)
+{
+ return stransport_wrap(out, in, host, 0);
+}
+
+int git_stransport_stream_new(git_stream **out, const char *host, const char *port)
+{
+ git_stream *stream = NULL;
+ int error;
+
+ GIT_ASSERT_ARG(out);
+ GIT_ASSERT_ARG(host);
+
+ error = git_socket_stream_new(&stream, host, port);
+
+ if (!error)
+ error = stransport_wrap(out, stream, host, 1);
+
+ if (error < 0 && stream) {
+ git_stream_close(stream);
+ git_stream_free(stream);
+ }
+
+ return error;
+}
+
+#endif