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 #include "streams/curl.h"
20
stransport_error(OSStatus ret)21 static int stransport_error(OSStatus ret)
22 {
23 CFStringRef message;
24
25 if (ret == noErr || ret == errSSLClosedGraceful) {
26 giterr_clear();
27 return 0;
28 }
29
30 #if !TARGET_OS_IPHONE
31 message = SecCopyErrorMessageString(ret, NULL);
32 GITERR_CHECK_ALLOC(message);
33
34 giterr_set(GITERR_NET, "SecureTransport error: %s", CFStringGetCStringPtr(message, kCFStringEncodingUTF8));
35 CFRelease(message);
36 #else
37 giterr_set(GITERR_NET, "SecureTransport error: OSStatus %d", (unsigned int)ret);
38 GIT_UNUSED(message);
39 #endif
40
41 return -1;
42 }
43
44 typedef struct {
45 git_stream parent;
46 git_stream *io;
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 ((error = git_stream_connect(st->io)) < 0)
61 return error;
62
63 ret = SSLHandshake(st->ctx);
64 if (ret != errSSLServerAuthCompleted) {
65 giterr_set(GITERR_SSL, "unexpected return value from ssl handshake %d", 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 giterr_set(GITERR_SSL, "internal security trust error");
82 return -1;
83 }
84
85 if (sec_res == kSecTrustResultDeny || sec_res == kSecTrustResultRecoverableTrustFailure ||
86 sec_res == kSecTrustResultFatalTrustFailure) {
87 giterr_set(GITERR_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 giterr_set(GITERR_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(io, data, *len, 0) < 0) {
153 return -36; /* "ioErr" from MacErrors.h which is not available on iOS */
154 }
155
156 return noErr;
157 }
158
stransport_write(git_stream * stream,const char * data,size_t len,int flags)159 static ssize_t stransport_write(git_stream *stream, const char *data, size_t len, int flags)
160 {
161 stransport_stream *st = (stransport_stream *) stream;
162 size_t data_len, processed;
163 OSStatus ret;
164
165 GIT_UNUSED(flags);
166
167 data_len = len;
168 if ((ret = SSLWrite(st->ctx, data, data_len, &processed)) != noErr)
169 return stransport_error(ret);
170
171 return 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 git_stream_close(st->io);
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 git_stream_free(st->io);
237 CFRelease(st->ctx);
238 if (st->der_data)
239 CFRelease(st->der_data);
240 git__free(st);
241 }
242
git_stransport_stream_new(git_stream ** out,const char * host,const char * port)243 int git_stransport_stream_new(git_stream **out, const char *host, const char *port)
244 {
245 stransport_stream *st;
246 int error;
247 OSStatus ret;
248
249 assert(out && host);
250
251 st = git__calloc(1, sizeof(stransport_stream));
252 GITERR_CHECK_ALLOC(st);
253
254 #ifdef GIT_CURL
255 error = git_curl_stream_new(&st->io, host, port);
256 #else
257 error = git_socket_stream_new(&st->io, host, port);
258 #endif
259
260 if (error < 0){
261 git__free(st);
262 return error;
263 }
264
265 st->ctx = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
266 if (!st->ctx) {
267 giterr_set(GITERR_NET, "failed to create SSL context");
268 git__free(st);
269 return -1;
270 }
271
272 if ((ret = SSLSetIOFuncs(st->ctx, read_cb, write_cb)) != noErr ||
273 (ret = SSLSetConnection(st->ctx, st->io)) != noErr ||
274 (ret = SSLSetSessionOption(st->ctx, kSSLSessionOptionBreakOnServerAuth, true)) != noErr ||
275 (ret = SSLSetProtocolVersionMin(st->ctx, kTLSProtocol1)) != noErr ||
276 (ret = SSLSetProtocolVersionMax(st->ctx, kTLSProtocol12)) != noErr ||
277 (ret = SSLSetPeerDomainName(st->ctx, host, strlen(host))) != noErr) {
278 CFRelease(st->ctx);
279 git__free(st);
280 return stransport_error(ret);
281 }
282
283 st->parent.version = GIT_STREAM_VERSION;
284 st->parent.encrypted = 1;
285 st->parent.proxy_support = git_stream_supports_proxy(st->io);
286 st->parent.connect = stransport_connect;
287 st->parent.certificate = stransport_certificate;
288 st->parent.set_proxy = stransport_set_proxy;
289 st->parent.read = stransport_read;
290 st->parent.write = stransport_write;
291 st->parent.close = stransport_close;
292 st->parent.free = stransport_free;
293
294 *out = (git_stream *) st;
295 return 0;
296 }
297
298 #endif
299