diff options
Diffstat (limited to 'src/libgit2/streams/stransport.c')
-rw-r--r-- | src/libgit2/streams/stransport.c | 326 |
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 |