stransport.c 7.88 KB
Newer Older
1 2 3 4 5 6 7
/*
 * 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.
 */

8
#include "streams/stransport.h"
9

10 11 12 13 14 15 16 17
#ifdef GIT_SECURE_TRANSPORT

#include <CoreFoundation/CoreFoundation.h>
#include <Security/SecureTransport.h>
#include <Security/SecCertificate.h>

#include "git2/transport.h"

18
#include "streams/socket.h"
19

20
static int stransport_error(OSStatus ret)
21
{
22 23
	CFStringRef message;

24
	if (ret == noErr || ret == errSSLClosedGraceful) {
25
		git_error_clear();
26 27
		return 0;
	}
28

29
#if !TARGET_OS_IPHONE
30
	message = SecCopyErrorMessageString(ret, NULL);
31
	GIT_ERROR_CHECK_ALLOC(message);
32

33
	git_error_set(GIT_ERROR_NET, "SecureTransport error: %s", CFStringGetCStringPtr(message, kCFStringEncodingUTF8));
34
	CFRelease(message);
35
#else
36
	git_error_set(GIT_ERROR_NET, "SecureTransport error: OSStatus %d", (unsigned int)ret);
37
	GIT_UNUSED(message);
38 39
#endif

40
	return -1;
41 42 43 44 45
}

typedef struct {
	git_stream parent;
	git_stream *io;
46
	int owned;
47 48 49 50 51
	SSLContextRef ctx;
	CFDataRef der_data;
	git_cert_x509 cert_info;
} stransport_stream;

52
static int stransport_connect(git_stream *stream)
53 54 55
{
	stransport_stream *st = (stransport_stream *) stream;
	int error;
56 57
	SecTrustRef trust = NULL;
	SecTrustResultType sec_res;
58 59
	OSStatus ret;

60
	if (st->owned && (error = git_stream_connect(st->io)) < 0)
61 62
		return error;

63 64
	ret = SSLHandshake(st->ctx);
	if (ret != errSSLServerAuthCompleted) {
65
		git_error_set(GIT_ERROR_SSL, "unexpected return value from ssl handshake %d", (int)ret);
66 67 68 69 70 71
		return -1;
	}

	if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr)
		goto on_error;

72 73 74
	if (!trust)
		return GIT_ECERTIFICATE;

75 76 77 78 79 80
	if ((ret = SecTrustEvaluate(trust, &sec_res)) != noErr)
		goto on_error;

	CFRelease(trust);

	if (sec_res == kSecTrustResultInvalid || sec_res == kSecTrustResultOtherError) {
81
		git_error_set(GIT_ERROR_SSL, "internal security trust error");
82 83 84 85
		return -1;
	}

	if (sec_res == kSecTrustResultDeny || sec_res == kSecTrustResultRecoverableTrustFailure ||
86
	    sec_res == kSecTrustResultFatalTrustFailure) {
87
		git_error_set(GIT_ERROR_SSL, "untrusted connection error");
88
		return GIT_ECERTIFICATE;
89
	}
90 91

	return 0;
92 93 94 95 96 97

on_error:
	if (trust)
		CFRelease(trust);

	return stransport_error(ret);
98 99
}

100
static int stransport_certificate(git_cert **out, git_stream *stream)
101 102 103 104 105 106 107 108 109 110 111 112 113 114
{
	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) {
115
		git_error_set(GIT_ERROR_SSL, "retrieved invalid certificate data");
116 117 118
		return -1;
	}

119
	st->cert_info.parent.cert_type = GIT_CERT_X509;
120 121 122 123 124 125 126
	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;
}

127
static int stransport_set_proxy(
128 129
	git_stream *stream,
	const git_proxy_options *proxy_opts)
130 131 132
{
	stransport_stream *st = (stransport_stream *) stream;

133
	return git_stream_set_proxy(st->io, proxy_opts);
134 135
}

136 137 138 139 140 141 142 143 144 145 146 147
/*
 * 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.
 */
148 149 150 151
static OSStatus write_cb(SSLConnectionRef conn, const void *data, size_t *len)
{
	git_stream *io = (git_stream *) conn;

152 153
	if (git_stream_write(io, data, *len, 0) < 0) {
		return -36; /* "ioErr" from MacErrors.h which is not available on iOS */
154 155 156 157 158
	}

	return noErr;
}

159
static ssize_t stransport_write(git_stream *stream, const char *data, size_t len, int flags)
160 161 162 163 164 165 166 167 168 169 170 171 172 173
{
	stransport_stream *st = (stransport_stream *) stream;
	size_t data_len, processed;
	OSStatus ret;

	GIT_UNUSED(flags);

	data_len = len;
	if ((ret = SSLWrite(st->ctx, data, data_len, &processed)) != noErr)
		return stransport_error(ret);

	return processed;
}

174 175 176 177 178 179 180 181 182
/*
 * 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).
 */
183 184 185
static OSStatus read_cb(SSLConnectionRef conn, void *data, size_t *len)
{
	git_stream *io = (git_stream *) conn;
186 187
	OSStatus error = noErr;
	size_t off = 0;
188 189 190
	ssize_t ret;

	do {
191
		ret = git_stream_read(io, data + off, *len - off);
192
		if (ret < 0) {
193 194 195 196 197 198
			error = -36; /* "ioErr" from MacErrors.h which is not available on iOS */
			break;
		}
		if (ret == 0) {
			error = errSSLClosedGraceful;
			break;
199 200
		}

201 202
		off += ret;
	} while (off < *len);
203

204 205
	*len = off;
	return error;
206 207
}

208
static ssize_t stransport_read(git_stream *stream, void *data, size_t len)
209 210 211 212 213 214 215 216 217 218 219
{
	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;
}

220
static int stransport_close(git_stream *stream)
221 222 223 224
{
	stransport_stream *st = (stransport_stream *) stream;
	OSStatus ret;

225 226
	ret = SSLClose(st->ctx);
	if (ret != noErr && ret != errSSLClosedGraceful)
227 228
		return stransport_error(ret);

229
	return st->owned ? git_stream_close(st->io) : 0;
230 231
}

232
static void stransport_free(git_stream *stream)
233 234 235
{
	stransport_stream *st = (stransport_stream *) stream;

236 237 238
	if (st->owned)
		git_stream_free(st->io);

239 240 241 242 243 244
	CFRelease(st->ctx);
	if (st->der_data)
		CFRelease(st->der_data);
	git__free(st);
}

245 246 247 248 249
static int stransport_wrap(
	git_stream **out,
	git_stream *in,
	const char *host,
	int owned)
250 251 252 253
{
	stransport_stream *st;
	OSStatus ret;

254
	assert(out && in && host);
255 256

	st = git__calloc(1, sizeof(stransport_stream));
257
	GIT_ERROR_CHECK_ALLOC(st);
258

259 260
	st->io = in;
	st->owned = owned;
261 262 263

	st->ctx = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
	if (!st->ctx) {
264
		git_error_set(GIT_ERROR_NET, "failed to create SSL context");
265
		git__free(st);
266 267 268 269 270
		return -1;
	}

	if ((ret = SSLSetIOFuncs(st->ctx, read_cb, write_cb)) != noErr ||
	    (ret = SSLSetConnection(st->ctx, st->io)) != noErr ||
271
	    (ret = SSLSetSessionOption(st->ctx, kSSLSessionOptionBreakOnServerAuth, true)) != noErr ||
272 273
	    (ret = SSLSetProtocolVersionMin(st->ctx, kTLSProtocol1)) != noErr ||
	    (ret = SSLSetProtocolVersionMax(st->ctx, kTLSProtocol12)) != noErr ||
274
	    (ret = SSLSetPeerDomainName(st->ctx, host, strlen(host))) != noErr) {
275 276
		CFRelease(st->ctx);
		git__free(st);
277 278 279 280 281
		return stransport_error(ret);
	}

	st->parent.version = GIT_STREAM_VERSION;
	st->parent.encrypted = 1;
282
	st->parent.proxy_support = git_stream_supports_proxy(st->io);
283 284
	st->parent.connect = stransport_connect;
	st->parent.certificate = stransport_certificate;
285
	st->parent.set_proxy = stransport_set_proxy;
286 287 288 289 290 291 292 293 294
	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;
}

295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
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;

	assert(out && 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;
}

323
#endif