86015e15a5
Reviewed-by: weijun, rhalade
319 lines
13 KiB
Java
319 lines
13 KiB
Java
/*
|
|
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
/*
|
|
* @test
|
|
* @bug 8279842 8282293
|
|
* @modules java.base/sun.security.util
|
|
* java.security.jgss/sun.security.jgss
|
|
* java.security.jgss/sun.security.jgss.krb5
|
|
* java.security.jgss/sun.security.jgss.krb5.internal
|
|
* java.security.jgss/sun.security.krb5.internal:+open
|
|
* java.security.jgss/sun.security.krb5:+open
|
|
* java.security.jgss/sun.security.krb5.internal.ccache
|
|
* java.security.jgss/sun.security.krb5.internal.crypto
|
|
* java.security.jgss/sun.security.krb5.internal.ktab
|
|
* jdk.security.auth
|
|
* jdk.security.jgss
|
|
* jdk.httpserver
|
|
* @summary HTTPS Channel Binding support for Java GSS/Kerberos
|
|
* @library /test/lib
|
|
* @run main jdk.test.lib.FileInstaller TestHosts TestHosts
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=always HttpsCB true true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=never HttpsCB false true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=invalid HttpsCB false true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* HttpsCB false true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=domain:other.com HttpsCB false true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=domain:host.web.domain HttpsCB true true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=domain:HOST.WEB.DOMAIN HttpsCB true true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=domain:*.web.domain HttpsCB true true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=domain:*.WEB.Domain HttpsCB true true
|
|
* @run main/othervm -Djdk.net.hosts.file=TestHosts
|
|
* -Djdk.https.negotiate.cbt=domain:*.Invalid,*.WEB.Domain HttpsCB true true
|
|
*/
|
|
|
|
import com.sun.net.httpserver.Headers;
|
|
import com.sun.net.httpserver.HttpExchange;
|
|
import com.sun.net.httpserver.HttpHandler;
|
|
import com.sun.net.httpserver.HttpServer;
|
|
import com.sun.net.httpserver.HttpPrincipal;
|
|
import com.sun.net.httpserver.HttpsConfigurator;
|
|
import com.sun.net.httpserver.HttpsExchange;
|
|
import com.sun.net.httpserver.HttpsServer;
|
|
import com.sun.security.auth.module.Krb5LoginModule;
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.PasswordAuthentication;
|
|
import java.net.Proxy;
|
|
import java.net.Socket;
|
|
import java.net.URL;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
import javax.net.ssl.SSLContext;
|
|
import javax.net.ssl.SSLEngine;
|
|
import javax.net.ssl.TrustManager;
|
|
import javax.net.ssl.X509ExtendedTrustManager;
|
|
import javax.security.auth.Subject;
|
|
|
|
import jdk.test.lib.Asserts;
|
|
import jdk.test.lib.net.SimpleSSLContext;
|
|
import org.ietf.jgss.GSSContext;
|
|
import org.ietf.jgss.GSSCredential;
|
|
import org.ietf.jgss.GSSManager;
|
|
import sun.security.jgss.GSSUtil;
|
|
import sun.security.jgss.krb5.internal.TlsChannelBindingImpl;
|
|
import sun.security.krb5.Config;
|
|
import sun.security.util.TlsChannelBinding;
|
|
|
|
import java.util.Base64;
|
|
import java.util.concurrent.Callable;
|
|
|
|
public class HttpsCB {
|
|
|
|
final static String REALM_WEB = "WEB.DOMAIN";
|
|
final static String KRB5_CONF = "web.conf";
|
|
final static String KRB5_TAB = "web.ktab";
|
|
|
|
final static String WEB_USER = "web";
|
|
final static char[] WEB_PASS = "webby".toCharArray();
|
|
final static String WEB_HOST = "host.web.domain";
|
|
final static String CONTENT = "Hello, World!";
|
|
|
|
static int webPort;
|
|
static URL cbtURL;
|
|
static URL normalURL;
|
|
|
|
public static void main(String[] args)
|
|
throws Exception {
|
|
|
|
boolean expectCBT = Boolean.parseBoolean(args[0]);
|
|
boolean expectNoCBT = Boolean.parseBoolean(args[1]);
|
|
|
|
System.setProperty("sun.security.krb5.debug", "true");
|
|
|
|
KDC kdcw = KDC.create(REALM_WEB);
|
|
kdcw.addPrincipal(WEB_USER, WEB_PASS);
|
|
kdcw.addPrincipalRandKey("krbtgt/" + REALM_WEB);
|
|
kdcw.addPrincipalRandKey("HTTP/" + WEB_HOST);
|
|
|
|
KDC.saveConfig(KRB5_CONF, kdcw,
|
|
"default_keytab_name = " + KRB5_TAB,
|
|
"[domain_realm]",
|
|
"",
|
|
".web.domain="+REALM_WEB);
|
|
|
|
System.setProperty("java.security.krb5.conf", KRB5_CONF);
|
|
Config.refresh();
|
|
KDC.writeMultiKtab(KRB5_TAB, kdcw);
|
|
|
|
// Write a customized JAAS conf file, so that any kinit cache
|
|
// will be ignored.
|
|
System.setProperty("java.security.auth.login.config", OneKDC.JAAS_CONF);
|
|
File f = new File(OneKDC.JAAS_CONF);
|
|
FileOutputStream fos = new FileOutputStream(f);
|
|
fos.write((
|
|
"com.sun.security.jgss.krb5.initiate {\n" +
|
|
" com.sun.security.auth.module.Krb5LoginModule required;\n};\n"
|
|
).getBytes());
|
|
fos.close();
|
|
|
|
HttpServer h1 = httpd("Negotiate",
|
|
"HTTP/" + WEB_HOST + "@" + REALM_WEB, KRB5_TAB);
|
|
webPort = h1.getAddress().getPort();
|
|
|
|
cbtURL = new URL("https://" + WEB_HOST +":" + webPort + "/cbt");
|
|
normalURL = new URL("https://" + WEB_HOST +":" + webPort + "/normal");
|
|
|
|
java.net.Authenticator.setDefault(new java.net.Authenticator() {
|
|
public PasswordAuthentication getPasswordAuthentication () {
|
|
return new PasswordAuthentication(
|
|
WEB_USER+"@"+REALM_WEB, WEB_PASS);
|
|
}
|
|
});
|
|
|
|
// Client-side SSLContext needs to ignore hostname mismatch
|
|
// and untrusted certificate.
|
|
SSLContext sc = SSLContext.getInstance("SSL");
|
|
sc.init(null, new TrustManager[] {
|
|
new X509ExtendedTrustManager() {
|
|
public X509Certificate[] getAcceptedIssuers() {
|
|
return null;
|
|
}
|
|
public void checkClientTrusted(X509Certificate[] chain,
|
|
String authType, Socket socket) { }
|
|
public void checkServerTrusted(X509Certificate[] chain,
|
|
String authType, Socket socket) { }
|
|
public void checkClientTrusted(X509Certificate[] chain,
|
|
String authType, SSLEngine engine) { }
|
|
public void checkServerTrusted(X509Certificate[] chain,
|
|
String authType, SSLEngine engine) { }
|
|
public void checkClientTrusted(X509Certificate[] certs,
|
|
String authType) { }
|
|
public void checkServerTrusted(X509Certificate[] certs,
|
|
String authType) { }
|
|
}
|
|
}, null);
|
|
|
|
Asserts.assertEQ(visit(sc, cbtURL), expectCBT);
|
|
Asserts.assertEQ(visit(sc, normalURL), expectNoCBT);
|
|
}
|
|
|
|
static boolean visit(SSLContext sc, URL url) {
|
|
try {
|
|
HttpsURLConnection conn = (HttpsURLConnection)
|
|
url.openConnection(Proxy.NO_PROXY);
|
|
conn.setSSLSocketFactory(sc.getSocketFactory());
|
|
BufferedReader reader;
|
|
reader = new BufferedReader(new InputStreamReader(
|
|
conn.getInputStream()));
|
|
return reader.readLine().equals(CONTENT);
|
|
} catch (IOException e) {
|
|
e.printStackTrace(System.out);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static HttpServer httpd(String scheme, String principal, String ktab)
|
|
throws Exception {
|
|
MyHttpHandler h = new MyHttpHandler();
|
|
HttpsServer server = HttpsServer.create(new InetSocketAddress(0), 0);
|
|
server.setHttpsConfigurator(
|
|
new HttpsConfigurator(new SimpleSSLContext().get()));
|
|
server.createContext("/", h).setAuthenticator(
|
|
new MyServerAuthenticator(scheme, principal, ktab));
|
|
server.start();
|
|
return server;
|
|
}
|
|
|
|
static class MyHttpHandler implements HttpHandler {
|
|
public void handle(HttpExchange t) throws IOException {
|
|
t.sendResponseHeaders(200, 0);
|
|
t.getResponseBody().write(CONTENT.getBytes());
|
|
t.close();
|
|
}
|
|
}
|
|
|
|
static class MyServerAuthenticator
|
|
extends com.sun.net.httpserver.Authenticator {
|
|
Subject s = new Subject();
|
|
GSSManager m;
|
|
GSSCredential cred;
|
|
String scheme = null;
|
|
String reqHdr = "WWW-Authenticate";
|
|
String respHdr = "Authorization";
|
|
int err = HttpURLConnection.HTTP_UNAUTHORIZED;
|
|
|
|
public MyServerAuthenticator(String scheme,
|
|
String principal, String ktab) throws Exception {
|
|
|
|
this.scheme = scheme;
|
|
Krb5LoginModule krb5 = new Krb5LoginModule();
|
|
Map<String, String> map = new HashMap<>();
|
|
Map<String, Object> shared = new HashMap<>();
|
|
|
|
map.put("storeKey", "true");
|
|
map.put("isInitiator", "false");
|
|
map.put("useKeyTab", "true");
|
|
map.put("keyTab", ktab);
|
|
map.put("principal", principal);
|
|
krb5.initialize(s, null, shared, map);
|
|
krb5.login();
|
|
krb5.commit();
|
|
m = GSSManager.getInstance();
|
|
cred = Subject.callAs(s, new Callable<GSSCredential>() {
|
|
@Override
|
|
public GSSCredential call() throws Exception {
|
|
System.err.println("Creating GSSCredential");
|
|
return m.createCredential(
|
|
null,
|
|
GSSCredential.INDEFINITE_LIFETIME,
|
|
MyServerAuthenticator.this.scheme
|
|
.equalsIgnoreCase("Negotiate") ?
|
|
GSSUtil.GSS_SPNEGO_MECH_OID :
|
|
GSSUtil.GSS_KRB5_MECH_OID,
|
|
GSSCredential.ACCEPT_ONLY);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public Result authenticate(HttpExchange exch) {
|
|
// The GSContext is stored in an HttpContext attribute named
|
|
// "GSSContext" and is created at the first request.
|
|
GSSContext c = null;
|
|
String auth = exch.getRequestHeaders().getFirst(respHdr);
|
|
try {
|
|
c = (GSSContext)exch.getHttpContext()
|
|
.getAttributes().get("GSSContext");
|
|
if (auth == null) { // First request
|
|
Headers map = exch.getResponseHeaders();
|
|
map.set (reqHdr, scheme); // Challenge!
|
|
c = Subject.callAs(s, () -> m.createContext(cred));
|
|
// CBT is required for cbtURL
|
|
if (exch instanceof HttpsExchange sexch
|
|
&& exch.getRequestURI().toString().equals("/cbt")) {
|
|
TlsChannelBinding b = TlsChannelBinding.create(
|
|
(X509Certificate) sexch.getSSLSession()
|
|
.getLocalCertificates()[0]);
|
|
c.setChannelBinding(
|
|
new TlsChannelBindingImpl(b.getData()));
|
|
}
|
|
exch.getHttpContext().getAttributes().put("GSSContext", c);
|
|
return new com.sun.net.httpserver.Authenticator.Retry(err);
|
|
} else { // Later requests
|
|
byte[] token = Base64.getMimeDecoder()
|
|
.decode(auth.split(" ")[1]);
|
|
token = c.acceptSecContext(token, 0, token.length);
|
|
Headers map = exch.getResponseHeaders();
|
|
map.set (reqHdr, scheme + " " + Base64.getMimeEncoder()
|
|
.encodeToString(token).replaceAll("\\s", ""));
|
|
if (c.isEstablished()) {
|
|
return new com.sun.net.httpserver.Authenticator.Success(
|
|
new HttpPrincipal(c.getSrcName().toString(), ""));
|
|
} else {
|
|
return new com.sun.net.httpserver.Authenticator.Retry(err);
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
}
|
|
}
|