1 /*
2  * Copyright (C) the libgit2 contributors. All rights reserved.
3  *
4  * This file is part of libgit2, distributed under the GNU GPL v2 with
5  * a Linking Exception. For full terms see the included COPYING file.
6  */
7 
8 #include "streams/stransport.h"
9 
10 #ifdef GIT_SECURE_TRANSPORT
11 
12 #include <CoreFoundation/CoreFoundation.h>
13 #include <Security/SecureTransport.h>
14 #include <Security/SecCertificate.h>
15 
16 #include "git2/transport.h"
17 
18 #include "streams/socket.h"
19 
stransport_error(OSStatus ret)20 static int stransport_error(OSStatus ret)
21 {
22 	CFStringRef message;
23 
24 	if (ret == noErr || ret == errSSLClosedGraceful) {
25 		git_error_clear();
26 		return 0;
27 	}
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 #endif
39 
40 	return -1;
41 }
42 
43 typedef struct {
44 	git_stream parent;
45 	git_stream *io;
46 	int owned;
47 	SSLContextRef ctx;
48 	CFDataRef der_data;
49 	git_cert_x509 cert_info;
50 } stransport_stream;
51 
stransport_connect(git_stream * stream)52 static int stransport_connect(git_stream *stream)
53 {
54 	stransport_stream *st = (stransport_stream *) stream;
55 	int error;
56 	SecTrustRef trust = NULL;
57 	SecTrustResultType sec_res;
58 	OSStatus ret;
59 
60 	if (st->owned && (error = git_stream_connect(st->io)) < 0)
61 		return error;
62 
63 	ret = SSLHandshake(st->ctx);
64 	if (ret != errSSLServerAuthCompleted) {
65 		git_error_set(GIT_ERROR_SSL, "unexpected return value from ssl handshake %d", (int)ret);
66 		return -1;
67 	}
68 
69 	if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr)
70 		goto on_error;
71 
72 	if (!trust)
73 		return GIT_ECERTIFICATE;
74 
75 	if ((ret = SecTrustEvaluate(trust, &sec_res)) != noErr)
76 		goto on_error;
77 
78 	CFRelease(trust);
79 
80 	if (sec_res == kSecTrustResultInvalid || sec_res == kSecTrustResultOtherError) {
81 		git_error_set(GIT_ERROR_SSL, "internal security trust error");
82 		return -1;
83 	}
84 
85 	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 on_error:
94 	if (trust)
95 		CFRelease(trust);
96 
97 	return stransport_error(ret);
98 }
99 
stransport_certificate(git_cert ** out,git_stream * stream)100 static int stransport_certificate(git_cert **out, git_stream *stream)
101 {
102 	stransport_stream *st = (stransport_stream *) stream;
103 	SecTrustRef trust = NULL;
104 	SecCertificateRef sec_cert;
105 	OSStatus ret;
106 
107 	if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr)
108 		return stransport_error(ret);
109 
110 	sec_cert = SecTrustGetCertificateAtIndex(trust, 0);
111 	st->der_data = SecCertificateCopyData(sec_cert);
112 	CFRelease(trust);
113 
114 	if (st->der_data == NULL) {
115 		git_error_set(GIT_ERROR_SSL, "retrieved invalid certificate data");
116 		return -1;
117 	}
118 
119 	st->cert_info.parent.cert_type = GIT_CERT_X509;
120 	st->cert_info.data = (void *) CFDataGetBytePtr(st->der_data);
121 	st->cert_info.len = CFDataGetLength(st->der_data);
122 
123 	*out = (git_cert *)&st->cert_info;
124 	return 0;
125 }
126 
stransport_set_proxy(git_stream * stream,const git_proxy_options * proxy_opts)127 static int stransport_set_proxy(
128 	git_stream *stream,
129 	const git_proxy_options *proxy_opts)
130 {
131 	stransport_stream *st = (stransport_stream *) stream;
132 
133 	return git_stream_set_proxy(st->io, proxy_opts);
134 }
135 
136 /*
137  * Contrary to typical network IO callbacks, Secure Transport write callback is
138  * expected to write *all* passed data, not just as much as it can, and any
139  * other case would be considered a failure.
140  *
141  * This behavior is actually not specified in the Apple documentation, but is
142  * required for things to work correctly (and incidentally, that's also how
143  * Apple implements it in its projects at opensource.apple.com).
144  *
145  * Libgit2 streams happen to already have this very behavior so this is just
146  * passthrough.
147  */
write_cb(SSLConnectionRef conn,const void * data,size_t * len)148 static OSStatus write_cb(SSLConnectionRef conn, const void *data, size_t *len)
149 {
150 	git_stream *io = (git_stream *) conn;
151 
152 	if (git_stream__write_full(io, data, *len, 0) < 0)
153 		return -36; /* "ioErr" from MacErrors.h which is not available on iOS */
154 
155 	return noErr;
156 }
157 
stransport_write(git_stream * stream,const char * data,size_t len,int flags)158 static ssize_t stransport_write(git_stream *stream, const char *data, size_t len, int flags)
159 {
160 	stransport_stream *st = (stransport_stream *) stream;
161 	size_t data_len, processed;
162 	OSStatus ret;
163 
164 	GIT_UNUSED(flags);
165 
166 	data_len = min(len, SSIZE_MAX);
167 	if ((ret = SSLWrite(st->ctx, data, data_len, &processed)) != noErr)
168 		return stransport_error(ret);
169 
170 	assert(processed < SSIZE_MAX);
171 	return (ssize_t)processed;
172 }
173 
174 /*
175  * Contrary to typical network IO callbacks, Secure Transport read callback is
176  * expected to read *exactly* the requested number of bytes, not just as much
177  * as it can, and any other case would be considered a failure.
178  *
179  * This behavior is actually not specified in the Apple documentation, but is
180  * required for things to work correctly (and incidentally, that's also how
181  * Apple implements it in its projects at opensource.apple.com).
182  */
read_cb(SSLConnectionRef conn,void * data,size_t * len)183 static OSStatus read_cb(SSLConnectionRef conn, void *data, size_t *len)
184 {
185 	git_stream *io = (git_stream *) conn;
186 	OSStatus error = noErr;
187 	size_t off = 0;
188 	ssize_t ret;
189 
190 	do {
191 		ret = git_stream_read(io, data + off, *len - off);
192 		if (ret < 0) {
193 			error = -36; /* "ioErr" from MacErrors.h which is not available on iOS */
194 			break;
195 		}
196 		if (ret == 0) {
197 			error = errSSLClosedGraceful;
198 			break;
199 		}
200 
201 		off += ret;
202 	} while (off < *len);
203 
204 	*len = off;
205 	return error;
206 }
207 
stransport_read(git_stream * stream,void * data,size_t len)208 static ssize_t stransport_read(git_stream *stream, void *data, size_t len)
209 {
210 	stransport_stream *st = (stransport_stream *) stream;
211 	size_t processed;
212 	OSStatus ret;
213 
214 	if ((ret = SSLRead(st->ctx, data, len, &processed)) != noErr)
215 		return stransport_error(ret);
216 
217 	return processed;
218 }
219 
stransport_close(git_stream * stream)220 static int stransport_close(git_stream *stream)
221 {
222 	stransport_stream *st = (stransport_stream *) stream;
223 	OSStatus ret;
224 
225 	ret = SSLClose(st->ctx);
226 	if (ret != noErr && ret != errSSLClosedGraceful)
227 		return stransport_error(ret);
228 
229 	return st->owned ? git_stream_close(st->io) : 0;
230 }
231 
stransport_free(git_stream * stream)232 static void stransport_free(git_stream *stream)
233 {
234 	stransport_stream *st = (stransport_stream *) stream;
235 
236 	if (st->owned)
237 		git_stream_free(st->io);
238 
239 	CFRelease(st->ctx);
240 	if (st->der_data)
241 		CFRelease(st->der_data);
242 	git__free(st);
243 }
244 
stransport_wrap(git_stream ** out,git_stream * in,const char * host,int owned)245 static int stransport_wrap(
246 	git_stream **out,
247 	git_stream *in,
248 	const char *host,
249 	int owned)
250 {
251 	stransport_stream *st;
252 	OSStatus ret;
253 
254 	assert(out && in && host);
255 
256 	st = git__calloc(1, sizeof(stransport_stream));
257 	GIT_ERROR_CHECK_ALLOC(st);
258 
259 	st->io = in;
260 	st->owned = owned;
261 
262 	st->ctx = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
263 	if (!st->ctx) {
264 		git_error_set(GIT_ERROR_NET, "failed to create SSL context");
265 		git__free(st);
266 		return -1;
267 	}
268 
269 	if ((ret = SSLSetIOFuncs(st->ctx, read_cb, write_cb)) != noErr ||
270 	    (ret = SSLSetConnection(st->ctx, st->io)) != noErr ||
271 	    (ret = SSLSetSessionOption(st->ctx, kSSLSessionOptionBreakOnServerAuth, true)) != noErr ||
272 	    (ret = SSLSetProtocolVersionMin(st->ctx, kTLSProtocol1)) != noErr ||
273 	    (ret = SSLSetProtocolVersionMax(st->ctx, kTLSProtocol12)) != noErr ||
274 	    (ret = SSLSetPeerDomainName(st->ctx, host, strlen(host))) != noErr) {
275 		CFRelease(st->ctx);
276 		git__free(st);
277 		return stransport_error(ret);
278 	}
279 
280 	st->parent.version = GIT_STREAM_VERSION;
281 	st->parent.encrypted = 1;
282 	st->parent.proxy_support = git_stream_supports_proxy(st->io);
283 	st->parent.connect = stransport_connect;
284 	st->parent.certificate = stransport_certificate;
285 	st->parent.set_proxy = stransport_set_proxy;
286 	st->parent.read = stransport_read;
287 	st->parent.write = stransport_write;
288 	st->parent.close = stransport_close;
289 	st->parent.free = stransport_free;
290 
291 	*out = (git_stream *) st;
292 	return 0;
293 }
294 
git_stransport_stream_wrap(git_stream ** out,git_stream * in,const char * host)295 int git_stransport_stream_wrap(
296 	git_stream **out,
297 	git_stream *in,
298 	const char *host)
299 {
300 	return stransport_wrap(out, in, host, 0);
301 }
302 
git_stransport_stream_new(git_stream ** out,const char * host,const char * port)303 int git_stransport_stream_new(git_stream **out, const char *host, const char *port)
304 {
305 	git_stream *stream = NULL;
306 	int error;
307 
308 	assert(out && host);
309 
310 	error = git_socket_stream_new(&stream, host, port);
311 
312 	if (!error)
313 		error = stransport_wrap(out, stream, host, 1);
314 
315 	if (error < 0 && stream) {
316 		git_stream_close(stream);
317 		git_stream_free(stream);
318 	}
319 
320 	return error;
321 }
322 
323 #endif
324