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 }