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   if (alternatives != null) {
93     auto count = sk_GENERAL_NAME_num(alternatives);
94     for (int i = 0; decision == Decision.SKIP && i < count; i++) {
95       auto name = sk_GENERAL_NAME_value(alternatives, i);
96       if (name is null) {
97         continue;
98       }
99       auto data = ASN1_STRING_data(name.d.ia5);
100       auto length = ASN1_STRING_length(name.d.ia5);
101       switch (name.type) {
102         case GENERAL_NAME.GEN_DNS:
103           decision = accessManager.verify(hostName, cast(char[])data[0 .. length]);
104           break;
105         case GENERAL_NAME.GEN_IPADD:
106           decision = accessManager.verify(peerAddress, data[0 .. length]);
107           break;
108         default:
109           // Do nothing.
110       }
111     }
112 
113     // DMD @@BUG@@: Empty template arguments parens should not be needed.
114     sk_GENERAL_NAME_pop_free!()(alternatives, &GENERAL_NAME_free);
115   }
116 
117   // If we are alredy done, return.
118   if (decision != Decision.SKIP) {
119     X509_free(cert);
120     if (decision != Decision.ALLOW) {
121       throw new TSSLException("Authorize: Access denied.");
122     }
123     return;
124   }
125 
126   // Check commonName.
127   auto name = X509_get_subject_name(cert);
128   if (name !is null) {
129     X509_NAME_ENTRY* entry;
130     char* utf8;
131     int last = -1;
132     while (decision == Decision.SKIP) {
133       last = X509_NAME_get_index_by_NID(name, NID_commonName, last);
134       if (last == -1)
135         break;
136       entry = X509_NAME_get_entry(name, last);
137       if (entry is null)
138         continue;
139       auto common = X509_NAME_ENTRY_get_data(entry);
140       auto size = ASN1_STRING_to_UTF8(&utf8, common);
141       decision = accessManager.verify(hostName, utf8[0 .. size]);
142       CRYPTO_free(utf8);
143     }
144   }
145   X509_free(cert);
146   if (decision != Decision.ALLOW) {
147     throw new TSSLException("Authorize: Could not authorize peer.");
148   }
149 }
150 
151 /*
152  * OpenSSL error information used for storing D exceptions on the OpenSSL
153  * error stack.
154  */
155 enum ERR_LIB_D_EXCEPTION = ERR_LIB_USER;
156 enum ERR_F_D_EXCEPTION = 0; // function id - what to use here?
157 enum ERR_R_D_EXCEPTION = 1234; // 99 and above are reserved for applications
158 enum ERR_FILE_D_EXCEPTION = "d_exception";
159 enum ERR_LINE_D_EXCEPTION = 0;
160 enum ERR_FLAGS_D_EXCEPTION = 0;
161 
162 /**
163  * Returns an exception for the last.
164  *
165  * Params:
166  *   location = An optional "location" to add to the error message (typically
167  *     the last SSL API call).
168  */
169 Exception getSSLException(string location = null, string clientFile = __FILE__,
170   size_t clientLine = __LINE__
171 ) {
172   // We can return either an exception saved from D BIO code, or a "true"
173   // OpenSSL error. Because there can possibly be more than one error on the
174   // error stack, we have to fetch all of them, and pick the last, i.e. newest
175   // one. We concatenate multiple successive OpenSSL error messages into a
176   // single one, but always just return the last D expcetion.
177   string message; // Probably better use an Appender here.
178   bool hadMessage;
179   Exception exception;
180 
181   void initMessage() {
182     message.destroy();
183     hadMessage = false;
184     if (!location.empty) {
185       message ~= location;
186       message ~= ": ";
187     }
188   }
189   initMessage();
190 
191   auto errn = errno;
192 
193   const(char)* file = void;
194   int line = void;
195   const(char)* data = void;
196   int flags = void;
197   c_ulong code = void;
198   while ((code = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
199     if (ERR_GET_REASON(code) == ERR_R_D_EXCEPTION) {
200       initMessage();
201       GC.removeRoot(cast(void*)data);
202       exception = cast(Exception)data;
203     } else {
204       exception = null;
205 
206       if (hadMessage) {
207         message ~= ", ";
208       }
209 
210       auto reason = ERR_reason_error_string(code);
211       if (reason) {
212         message ~= "SSL error: " ~ to!string(reason);
213       } else {
214         message ~= "SSL error #" ~ to!string(code);
215       }
216 
217       hadMessage = true;
218     }
219   }
220 
221   // If the last item from the stack was a D exception, throw it.
222   if (exception) return exception;
223 
224   // We are dealing with an OpenSSL error that doesn't root in a D exception.
225   if (!hadMessage) {
226     // If we didn't get an actual error from the stack yet, try errno.
227     string errnString;
228     if (errn != 0) {
229       errnString = to!string(strerror(errn));
230     }
231     if (errnString.empty) {
232       message ~= "Unknown error";
233     } else {
234       message ~= errnString;
235     }
236   }
237 
238   message ~= ".";
239   return new TSSLException(message, clientFile, clientLine);
240 }