1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements. See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership. The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License. You may obtain a copy of the License at
9  *
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied. See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 module thrift.internal.ssl;
20 
21 import core.memory : GC;
22 import core.stdc.config;
23 import core.stdc.errno : errno;
24 import core.stdc.string : strerror;
25 import deimos.openssl.err;
26 import deimos.openssl.ssl;
27 import deimos.openssl.x509v3;
28 import std.array : empty, appender;
29 import std.conv : to;
30 import std.socket : Address;
31 import thrift.transport.ssl;
32 
33 /**
34  * Checks if the peer is authorized after the SSL handshake has been
35  * completed on the given conncetion and throws an TSSLException if not.
36  *
37  * Params:
38  *   ssl = The SSL connection to check.
39  *   accessManager = The access manager to check the peer againts.
40  *   peerAddress = The (IP) address of the peer.
41  *   hostName = The host name of the peer.
42  */
43 void authorize(SSL* ssl, TAccessManager accessManager,
44   Address peerAddress, lazy string hostName
45 ) {
46   alias TAccessManager.Decision Decision;
47 
48   auto rc = SSL_get_verify_result(ssl);
49   if (rc != X509_V_OK) {
50     throw new TSSLException("SSL_get_verify_result(): " ~
51       to!string(X509_verify_cert_error_string(rc)));
52   }
53 
54   auto cert = SSL_get_peer_certificate(ssl);
55   if (cert is null) {
56     // Certificate is not present.
57     if (SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) {
58       throw new TSSLException(
59         "Authorize: Required certificate not present.");
60     }
61 
62     // If we don't have an access manager set, we don't intend to authorize
63     // the client, so everything's fine.
64     if (accessManager) {
65       throw new TSSLException(
66         "Authorize: Certificate required for authorization.");
67     }
68     return;
69   }
70 
71   if (accessManager is null) {
72     // No access manager set, can return immediately as the cert is valid
73     // and all peers are authorized.
74     X509_free(cert);
75     return;
76   }
77 
78   // both certificate and access manager are present
79   auto decision = accessManager.verify(peerAddress);
80 
81   if (decision != Decision.SKIP) {
82     X509_free(cert);
83     if (decision != Decision.ALLOW) {
84       throw new TSSLException("Authorize: Access denied based on remote IP.");
85     }
86     return;
87   }
88 
89   // Check subjectAltName(s), if present.
90   auto alternatives = cast(STACK_OF!(GENERAL_NAME)*)
91     X509_get_ext_d2i(cert, NID_subject_alt_name, null, null);
92 
93   version(use_openssl_1_0_x) {
94     enum _GEN_DNS = GENERAL_NAME.GEN_DNS;
95     enum _GEN_IPADD = GENERAL_NAME.GEN_IPADD;
96   } else version(use_openssl_1_1_x) {
97     enum _GEN_DNS = GEN_DNS;
98     enum _GEN_IPADD = GEN_IPADD;
99   } else {
100     static assert(false, `Must have version either use_openssl_1_0_x or use_openssl_1_1_x defined, e.g.
101 	"subConfigurations": {
102 		"apache-thrift": "use_openssl_1_0"
103 	}`);
104   }
105 
106   if (alternatives != null) {
107     auto count = sk_GENERAL_NAME_num(alternatives);
108     for (int i = 0; decision == Decision.SKIP && i < count; i++) {
109       auto name = sk_GENERAL_NAME_value(alternatives, i);
110       if (name is null) {
111         continue;
112       }
113       auto data = ASN1_STRING_data(name.d.ia5);
114       auto length = ASN1_STRING_length(name.d.ia5);
115 
116       switch (name.type) {
117         case _GEN_DNS:
118           decision = accessManager.verify(hostName, cast(char[])data[0 .. length]);
119           break;
120         case _GEN_IPADD:
121           decision = accessManager.verify(peerAddress, data[0 .. length]);
122           break;
123         default:
124           // Do nothing.
125       }
126     }
127 
128     // DMD @@BUG@@: Empty template arguments parens should not be needed.
129     sk_GENERAL_NAME_pop_free!()(alternatives, &GENERAL_NAME_free);
130   }
131 
132   // If we are alredy done, return.
133   if (decision != Decision.SKIP) {
134     X509_free(cert);
135     if (decision != Decision.ALLOW) {
136       throw new TSSLException("Authorize: Access denied.");
137     }
138     return;
139   }
140 
141   // Check commonName.
142   auto name = X509_get_subject_name(cert);
143   if (name !is null) {
144     X509_NAME_ENTRY* entry;
145     char* utf8;
146     int last = -1;
147     while (decision == Decision.SKIP) {
148       last = X509_NAME_get_index_by_NID(name, NID_commonName, last);
149       if (last == -1)
150         break;
151       entry = X509_NAME_get_entry(name, last);
152       if (entry is null)
153         continue;
154       auto common = X509_NAME_ENTRY_get_data(entry);
155       auto size = ASN1_STRING_to_UTF8(&utf8, common);
156       decision = accessManager.verify(hostName, utf8[0 .. size]);
157       CRYPTO_free(utf8);
158     }
159   }
160   X509_free(cert);
161   if (decision != Decision.ALLOW) {
162     throw new TSSLException("Authorize: Could not authorize peer.");
163   }
164 }
165 
166 /*
167  * OpenSSL error information used for storing D exceptions on the OpenSSL
168  * error stack.
169  */
170 enum ERR_LIB_D_EXCEPTION = ERR_LIB_USER;
171 enum ERR_F_D_EXCEPTION = 0; // function id - what to use here?
172 enum ERR_R_D_EXCEPTION = 1234; // 99 and above are reserved for applications
173 enum ERR_FILE_D_EXCEPTION = "d_exception";
174 enum ERR_LINE_D_EXCEPTION = 0;
175 enum ERR_FLAGS_D_EXCEPTION = 0;
176 
177 /**
178  * Returns an exception for the last.
179  *
180  * Params:
181  *   location = An optional "location" to add to the error message (typically
182  *     the last SSL API call).
183  */
184 Exception getSSLException(string location = null, string clientFile = __FILE__,
185   size_t clientLine = __LINE__
186 ) {
187   // We can return either an exception saved from D BIO code, or a "true"
188   // OpenSSL error. Because there can possibly be more than one error on the
189   // error stack, we have to fetch all of them, and pick the last, i.e. newest
190   // one. We concatenate multiple successive OpenSSL error messages into a
191   // single one, but always just return the last D expcetion.
192   string message; // Probably better use an Appender here.
193   bool hadMessage;
194   Exception exception;
195 
196   void initMessage() {
197     message.destroy();
198     hadMessage = false;
199     if (!location.empty) {
200       message ~= location;
201       message ~= ": ";
202     }
203   }
204   initMessage();
205 
206   auto errn = errno;
207 
208   const(char)* file = void;
209   int line = void;
210   const(char)* data = void;
211   int flags = void;
212   c_ulong code = void;
213   while ((code = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
214     if (ERR_GET_REASON(code) == ERR_R_D_EXCEPTION) {
215       initMessage();
216       GC.removeRoot(cast(void*)data);
217       exception = cast(Exception)data;
218     } else {
219       exception = null;
220 
221       if (hadMessage) {
222         message ~= ", ";
223       }
224 
225       auto reason = ERR_reason_error_string(code);
226       if (reason) {
227         message ~= "SSL error: " ~ to!string(reason);
228       } else {
229         message ~= "SSL error #" ~ to!string(code);
230       }
231 
232       hadMessage = true;
233     }
234   }
235 
236   // If the last item from the stack was a D exception, throw it.
237   if (exception) return exception;
238 
239   // We are dealing with an OpenSSL error that doesn't root in a D exception.
240   if (!hadMessage) {
241     // If we didn't get an actual error from the stack yet, try errno.
242     string errnString;
243     if (errn != 0) {
244       errnString = to!string(strerror(errn));
245     }
246     if (errnString.empty) {
247       message ~= "Unknown error";
248     } else {
249       message ~= errnString;
250     }
251   }
252 
253   message ~= ".";
254   return new TSSLException(message, clientFile, clientLine);
255 }