4
4
* 2.0; you may not use this file except in compliance with the Elastic License
5
5
* 2.0.
6
6
*/
7
- package org .elasticsearch .xpack .security . tool ;
7
+ package org .elasticsearch .xpack .core . security ;
8
8
9
+ import org .elasticsearch .common .hash .MessageDigests ;
9
10
import org .elasticsearch .common .io .Streams ;
11
+ import org .elasticsearch .core .CharArrays ;
10
12
import org .elasticsearch .core .CheckedFunction ;
11
13
import org .elasticsearch .common .CheckedSupplier ;
12
14
import org .elasticsearch .common .Strings ;
23
25
import org .elasticsearch .xpack .core .common .socket .SocketAccess ;
24
26
import org .elasticsearch .xpack .core .security .authc .support .UsernamePasswordToken ;
25
27
import org .elasticsearch .xpack .core .ssl .SSLService ;
26
- import org .elasticsearch .xpack .security . tool .HttpResponse .HttpResponseBuilder ;
28
+ import org .elasticsearch .xpack .core . security .HttpResponse .HttpResponseBuilder ;
27
29
28
30
import javax .net .ssl .HttpsURLConnection ;
31
+ import javax .net .ssl .SSLContext ;
32
+ import javax .net .ssl .TrustManager ;
33
+ import javax .net .ssl .X509TrustManager ;
29
34
import java .io .IOException ;
30
35
import java .io .InputStream ;
31
36
import java .io .OutputStream ;
34
39
import java .net .MalformedURLException ;
35
40
import java .net .URISyntaxException ;
36
41
import java .net .URL ;
42
+ import java .nio .CharBuffer ;
37
43
import java .nio .charset .StandardCharsets ;
38
44
import java .security .AccessController ;
39
- import java .security .PrivilegedAction ;
45
+ import java .security .MessageDigest ;
46
+ import java .security .PrivilegedExceptionAction ;
47
+ import java .security .cert .Certificate ;
48
+ import java .security .cert .CertificateException ;
49
+ import java .security .cert .X509Certificate ;
50
+ import java .util .Arrays ;
51
+ import java .util .Base64 ;
40
52
import java .util .Collections ;
41
53
import java .util .List ;
42
54
import java .util .Map ;
@@ -60,9 +72,16 @@ public class CommandLineHttpClient {
60
72
private static final int READ_TIMEOUT = 35 * 1000 ;
61
73
62
74
private final Environment env ;
75
+ private final String pinnedCaCertFingerprint ;
63
76
64
77
public CommandLineHttpClient (Environment env ) {
65
78
this .env = env ;
79
+ this .pinnedCaCertFingerprint = null ;
80
+ }
81
+
82
+ public CommandLineHttpClient (Environment env , String pinnedCaCertFingerprint ) {
83
+ this .env = env ;
84
+ this .pinnedCaCertFingerprint = pinnedCaCertFingerprint ;
66
85
}
67
86
68
87
/**
@@ -79,22 +98,55 @@ public CommandLineHttpClient(Environment env) {
79
98
* handler of the response Input Stream.
80
99
* @return HTTP protocol response code.
81
100
*/
82
- @ SuppressForbidden (reason = "We call connect in doPrivileged and provide SocketPermission" )
83
101
public HttpResponse execute (String method , URL url , String user , SecureString password ,
84
102
CheckedSupplier <String , Exception > requestBodySupplier ,
85
103
CheckedFunction <InputStream , HttpResponseBuilder , Exception > responseHandler ) throws Exception {
104
+
105
+ final String authorizationHeader = UsernamePasswordToken .basicAuthHeaderValue (user , password );
106
+ return execute (method , url , authorizationHeader , requestBodySupplier , responseHandler );
107
+ }
108
+
109
+ /**
110
+ * General purpose HTTP(S) call with JSON Content-Type and Authorization Header.
111
+ * SSL settings are read from the settings file, if any.
112
+ *
113
+ * @param apiKey
114
+ * API key value to be used in the Authorization header
115
+ * @param requestBodySupplier
116
+ * supplier for the JSON string body of the request.
117
+ * @param responseHandler
118
+ * handler of the response Input Stream.
119
+ * @return HTTP protocol response code.
120
+ */
121
+ public HttpResponse execute (String method , URL url , SecureString apiKey ,
122
+ CheckedSupplier <String , Exception > requestBodySupplier ,
123
+ CheckedFunction <InputStream , HttpResponseBuilder , Exception > responseHandler ) throws Exception {
124
+ final String authorizationHeaderValue = apiKeyHeaderValue (apiKey );
125
+ return execute (method , url , authorizationHeaderValue , requestBodySupplier , responseHandler );
126
+ }
127
+
128
+ @ SuppressForbidden (reason = "We call connect in doPrivileged and provide SocketPermission" )
129
+ private HttpResponse execute (String method , URL url , String authorizationHeader ,
130
+ CheckedSupplier <String , Exception > requestBodySupplier ,
131
+ CheckedFunction <InputStream , HttpResponseBuilder , Exception > responseHandler ) throws Exception {
86
132
final HttpURLConnection conn ;
87
133
// If using SSL, need a custom service because it's likely a self-signed certificate
88
134
if ("https" .equalsIgnoreCase (url .getProtocol ())) {
89
135
final SSLService sslService = new SSLService (env );
90
136
final HttpsURLConnection httpsConn = (HttpsURLConnection ) url .openConnection ();
91
- AccessController .doPrivileged ((PrivilegedAction <Void >) () -> {
92
- final SslConfiguration sslConfiguration = sslService .getHttpTransportSSLConfiguration ();
93
- // Requires permission java.lang.RuntimePermission "setFactory";
94
- httpsConn .setSSLSocketFactory (sslService .sslSocketFactory (sslConfiguration ));
95
- final boolean isHostnameVerificationEnabled = sslConfiguration .getVerificationMode ().isHostnameVerificationEnabled ();
96
- if (isHostnameVerificationEnabled == false ) {
97
- httpsConn .setHostnameVerifier ((hostname , session ) -> true );
137
+ AccessController .doPrivileged ((PrivilegedExceptionAction <Void >) () -> {
138
+ if (pinnedCaCertFingerprint != null ) {
139
+ final SSLContext sslContext = SSLContext .getInstance ("TLS" );
140
+ sslContext .init (null , new TrustManager [] { fingerprintTrustingTrustManager (pinnedCaCertFingerprint ) }, null );
141
+ httpsConn .setSSLSocketFactory (sslContext .getSocketFactory ());
142
+ } else {
143
+ final SslConfiguration sslConfiguration = sslService .getHttpTransportSSLConfiguration ();
144
+ // Requires permission java.lang.RuntimePermission "setFactory";
145
+ httpsConn .setSSLSocketFactory (sslService .sslSocketFactory (sslConfiguration ));
146
+ final boolean isHostnameVerificationEnabled = sslConfiguration .getVerificationMode ().isHostnameVerificationEnabled ();
147
+ if (isHostnameVerificationEnabled == false ) {
148
+ httpsConn .setHostnameVerifier ((hostname , session ) -> true );
149
+ }
98
150
}
99
151
return null ;
100
152
});
@@ -105,8 +157,7 @@ public HttpResponse execute(String method, URL url, String user, SecureString pa
105
157
conn .setRequestMethod (method );
106
158
conn .setReadTimeout (READ_TIMEOUT );
107
159
// Add basic-auth header
108
- String token = UsernamePasswordToken .basicAuthHeaderValue (user , password );
109
- conn .setRequestProperty ("Authorization" , token );
160
+ conn .setRequestProperty ("Authorization" , authorizationHeader );
110
161
conn .setRequestProperty ("Content-Type" , XContentType .JSON .mediaType ());
111
162
String bodyString = requestBodySupplier .get ();
112
163
conn .setDoOutput (bodyString != null ); // set true if we are sending a body
@@ -253,4 +304,48 @@ public static HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) t
253
304
public static URL createURL (URL url , String path , String query ) throws MalformedURLException , URISyntaxException {
254
305
return new URL (url , (url .toURI ().getPath () + path ).replaceAll ("/+" , "/" ) + query );
255
306
}
307
+
308
+ public static String apiKeyHeaderValue (SecureString apiKey ) {
309
+ CharBuffer chars = CharBuffer .allocate (apiKey .length ());
310
+ byte [] charBytes = null ;
311
+ try {
312
+ chars .put (apiKey .getChars ());
313
+ charBytes = CharArrays .toUtf8Bytes (chars .array ());
314
+
315
+ //TODO we still have passwords in Strings in headers. Maybe we can look into using a CharSequence?
316
+ String apiKeyToken = Base64 .getEncoder ().encodeToString (charBytes );
317
+ return "ApiKey " + apiKeyToken ;
318
+ } finally {
319
+ Arrays .fill (chars .array (), (char ) 0 );
320
+ if (charBytes != null ) {
321
+ Arrays .fill (charBytes , (byte ) 0 );
322
+ }
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Returns a TrustManager to be used in a client SSLContext, which trusts all certificates that are signed
328
+ * by a specific CA certificate ( identified by its SHA256 fingerprint, {@code pinnedCaCertFingerPrint} )
329
+ */
330
+ private TrustManager fingerprintTrustingTrustManager (String pinnedCaCertFingerprint ) {
331
+ final TrustManager trustManager = new X509TrustManager () {
332
+ public void checkClientTrusted (X509Certificate [] chain , String authType ) throws CertificateException {
333
+ }
334
+
335
+ public void checkServerTrusted (X509Certificate [] chain , String authType ) throws CertificateException {
336
+ final Certificate caCertFromChain = chain [1 ];
337
+ MessageDigest sha256 = MessageDigests .sha256 ();
338
+ sha256 .update (caCertFromChain .getEncoded ());
339
+ if (MessageDigests .toHexString (sha256 .digest ()).equals (pinnedCaCertFingerprint ) == false ) {
340
+ throw new CertificateException ();
341
+ }
342
+ }
343
+
344
+ @ Override public X509Certificate [] getAcceptedIssuers () {
345
+ return new X509Certificate [0 ];
346
+ }
347
+ };
348
+
349
+ return trustManager ;
350
+ }
256
351
}
0 commit comments