diff --git a/src/main/java/com/aliyun/gmsse/ClientHandshakeProtocol.java b/src/main/java/com/aliyun/gmsse/ClientHandshakeProtocol.java new file mode 100644 index 0000000..d3fbae6 --- /dev/null +++ b/src/main/java/com/aliyun/gmsse/ClientHandshakeProtocol.java @@ -0,0 +1,13 @@ +package com.aliyun.gmsse; + +public class ClientHandshakeProtocol extends HandshakeProtocol { + + public ClientHandshakeProtocol(GMSSLContextSpi context) { + + } + + public void connect() { + + } + +} diff --git a/src/main/java/com/aliyun/gmsse/ConnectionContext.java b/src/main/java/com/aliyun/gmsse/ConnectionContext.java index 1c588e7..5189534 100644 --- a/src/main/java/com/aliyun/gmsse/ConnectionContext.java +++ b/src/main/java/com/aliyun/gmsse/ConnectionContext.java @@ -2,6 +2,8 @@ import java.io.IOException; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; + public abstract class ConnectionContext { protected GMSSLContextSpi sslContext; @@ -18,4 +20,45 @@ public ConnectionContext(GMSSLContextSpi context, GMSSLSocket socket, SSLConfigu } public abstract void kickstart() throws IOException; + + public void setUseClientMode(boolean mode) { + sslConfig.isClientMode = mode; + } + + public HandshakeStatus getHandshakeStatus() { + // if (!outputRecord.isEmpty()) { + // // If no handshaking, special case to wrap alters or + // // post-handshake messages. + // return HandshakeStatus.NEED_WRAP; + // } else if (isOutboundClosed() && isInboundClosed()) { + // return HandshakeStatus.NOT_HANDSHAKING; + // } else if (handshakeContext != null) { + // if (!handshakeContext.delegatedActions.isEmpty()) { + // return HandshakeStatus.NEED_TASK; + // } else if (!isInboundClosed()) { + // //JDK8 NEED_UNWRAP returnned for NEED_UNWRAP_AGAIN status + // // needUnwrapAgain should be used to determine NEED_UNWRAP_AGAIN + // return HandshakeStatus.NEED_UNWRAP; + // } else if (!isOutboundClosed()) { + // // Special case that the inbound was closed, but outbound open. + // return HandshakeStatus.NEED_WRAP; + // } + // } else if (isOutboundClosed() && !isInboundClosed()) { + // // Special case that the outbound was closed, but inbound open. + // return HandshakeStatus.NEED_UNWRAP; + // } else if (!isOutboundClosed() && isInboundClosed()) { + // // Special case that the inbound was closed, but outbound open. + // return HandshakeStatus.NEED_WRAP; + // } + + return HandshakeStatus.NOT_HANDSHAKING; + } + + public boolean isInboundClosed() { + return false; + } + + public boolean isOutboundDone() { + return false; + } } diff --git a/src/main/java/com/aliyun/gmsse/GMSSLContextSpi.java b/src/main/java/com/aliyun/gmsse/GMSSLContextSpi.java index 84d5606..aea561a 100644 --- a/src/main/java/com/aliyun/gmsse/GMSSLContextSpi.java +++ b/src/main/java/com/aliyun/gmsse/GMSSLContextSpi.java @@ -46,12 +46,12 @@ public GMSSLContextSpi() { @Override protected SSLEngine engineCreateSSLEngine() { - return null; + return new GMSSLEngine(this); } @Override protected SSLEngine engineCreateSSLEngine(String host, int port) { - return null; + return new GMSSLEngine(this, host, port); } @Override diff --git a/src/main/java/com/aliyun/gmsse/GMSSLEngine.java b/src/main/java/com/aliyun/gmsse/GMSSLEngine.java new file mode 100644 index 0000000..1d5b997 --- /dev/null +++ b/src/main/java/com/aliyun/gmsse/GMSSLEngine.java @@ -0,0 +1,182 @@ +package com.aliyun.gmsse; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; + +import com.aliyun.gmsse.protocol.ClientConnectionContext; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +public class GMSSLEngine extends SSLEngine { + + private GMSSLContextSpi context; + private ConnectionContext connection; + private HandshakeProtocol protocol; + + public GMSSLEngine(GMSSLContextSpi context, String host, int port) { + super(host, port); + this.context = context; + this.connection = new ClientConnectionContext(context, null); + } + + public GMSSLEngine(GMSSLContextSpi context) { + this(context, null, -1); + } + + @Override + public void beginHandshake() throws SSLException { + if (getUseClientMode()) { + ClientHandshakeProtocol clientProtocol = new ClientHandshakeProtocol(context); + clientProtocol.connect(); + this.protocol = clientProtocol; + } else { + // TODO: server side + } + } + + @Override + public void closeInbound() throws SSLException { + try { + this.protocol.closeInbound(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public void closeOutbound() { + try { + this.protocol.closeOutbound(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public Runnable getDelegatedTask() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean getEnableSessionCreation() { + return connection.sslConfig.enableSessionCreation; + } + + @Override + public String[] getEnabledCipherSuites() { + return CipherSuite.namesOf(connection.sslConfig.enabledCipherSuites); + } + + @Override + public String[] getEnabledProtocols() { + return ProtocolVersion.toStringArray(connection.sslConfig.enabledProtocols); + } + + @Override + public HandshakeStatus getHandshakeStatus() { + return connection.getHandshakeStatus(); + } + + @Override + public boolean getNeedClientAuth() { + // TODO Auto-generated method stub + return false; + } + + @Override + public SSLSession getSession() { + return connection.session; + } + + @Override + public String[] getSupportedCipherSuites() { + return CipherSuite.namesOf(context.getSupportedCipherSuites()); + } + + @Override + public String[] getSupportedProtocols() { + return ProtocolVersion.toStringArray(context.getSupportedProtocolVersions()); + } + + @Override + public boolean getUseClientMode() { + return connection.sslConfig.isClientMode; + } + + @Override + public boolean getWantClientAuth() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isInboundDone() { + return connection.isInboundClosed(); + } + + @Override + public boolean isOutboundDone() { + return connection.isOutboundDone(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + connection.sslConfig.enableSessionCreation = flag; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + // TODO Auto-generated method stub + } + + @Override + public void setEnabledProtocols(String[] arg0) { + // TODO Auto-generated method stub + } + + @Override + public void setNeedClientAuth(boolean need) { + // TODO Auto-generated method stub + } + + @Override + public void setUseClientMode(boolean mode) { + connection.setUseClientMode(mode); + } + + @Override + public void setWantClientAuth(boolean want) { + // TODO Auto-generated method stub + } + + @Override + public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) throws SSLException { + return unwrap(new ByteBuffer[]{src}, 0, 1, dsts, offset, length); + } + + private SSLEngineResult unwrap(ByteBuffer[] srcs, int srcsOffset, int srcslength, ByteBuffer[] dsts, int dstsOffset, int dstsLength) { + if (isInboundDone()) { + return new SSLEngineResult(Status.CLOSED, getHandshakeStatus(), 0, 0, -1); + } + return null; + } + + @Override + public SSLEngineResult wrap(ByteBuffer[] appData, int offset, int length, ByteBuffer netData) throws SSLException { + return wrap(appData, offset, length, new ByteBuffer[]{ netData }, 0, 1); + } + + private SSLEngineResult wrap(ByteBuffer[] srcs, int srcsOffset, int srcsLength, ByteBuffer[] dsts, int dstsOffset, int dstsLength) { + return null; + } + +} diff --git a/src/main/java/com/aliyun/gmsse/HandshakeProtocol.java b/src/main/java/com/aliyun/gmsse/HandshakeProtocol.java new file mode 100644 index 0000000..d58e2d3 --- /dev/null +++ b/src/main/java/com/aliyun/gmsse/HandshakeProtocol.java @@ -0,0 +1,16 @@ +package com.aliyun.gmsse; + +import java.io.IOException; + +public class HandshakeProtocol { + + private RecordStream recordStream; + + public void closeInbound() throws IOException { + this.recordStream.getInputStream().close(); + } + + public void closeOutbound() throws IOException { + this.recordStream.getOutputStream().close(); + } +} diff --git a/src/main/java/com/aliyun/gmsse/RecordStream.java b/src/main/java/com/aliyun/gmsse/RecordStream.java index d1893be..9619fdf 100644 --- a/src/main/java/com/aliyun/gmsse/RecordStream.java +++ b/src/main/java/com/aliyun/gmsse/RecordStream.java @@ -290,4 +290,12 @@ synchronized long nextValue() throws AlertException { } } + public OutputStream getOutputStream() { + return output; + } + + public InputStream getInputStream() { + return input; + } + } diff --git a/src/main/java/com/aliyun/gmsse/ServerHandshakeProtocol.java b/src/main/java/com/aliyun/gmsse/ServerHandshakeProtocol.java new file mode 100644 index 0000000..81c727c --- /dev/null +++ b/src/main/java/com/aliyun/gmsse/ServerHandshakeProtocol.java @@ -0,0 +1,5 @@ +package com.aliyun.gmsse; + +public class ServerHandshakeProtocol { + +} diff --git a/src/test/java/com/aliyun/gmsse/EngineTest.java b/src/test/java/com/aliyun/gmsse/EngineTest.java new file mode 100644 index 0000000..da3a700 --- /dev/null +++ b/src/test/java/com/aliyun/gmsse/EngineTest.java @@ -0,0 +1,388 @@ +package com.aliyun.gmsse; + +/* + * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * -Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Oracle nor the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. + * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN + * OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR + * FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or + * intended for use in the design, construction, operation or + * maintenance of any nuclear facility. + */ + +/** + * A SSLEngine usage example which simplifies the presentation + * by removing the I/O and multi-threading concerns. + * + * The demo creates two SSLEngines, simulating a client and server. + * The "transport" layer consists two ByteBuffers: think of them + * as directly connected pipes. + * + * Note, this is a *very* simple example: real code will be much more + * involved. For example, different threading and I/O models could be + * used, transport mechanisms could close unexpectedly, and so on. + * + * When this application runs, notice that several messages + * (wrap/unwrap) pass before any application data is consumed or + * produced. (For more information, please see the SSL/TLS + * specifications.) There may several steps for a successful handshake, + * so it's typical to see the following series of operations: + * + * client server message + * ====== ====== ======= + * wrap() ... ClientHello + * ... unwrap() ClientHello + * ... wrap() ServerHello/Certificate + * unwrap() ... ServerHello/Certificate + * wrap() ... ClientKeyExchange + * wrap() ... ChangeCipherSpec + * wrap() ... Finished + * ... unwrap() ClientKeyExchange + * ... unwrap() ChangeCipherSpec + * ... unwrap() Finished + * ... wrap() ChangeCipherSpec + * ... wrap() Finished + * unwrap() ... ChangeCipherSpec + * unwrap() ... Finished + */ + +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.*; +import java.io.*; +import java.security.*; +import java.nio.*; + +public class EngineTest { + + /* + * Enables logging of the SSLEngine operations. + */ + private static boolean logging = true; + + /* + * Enables the JSSE system debugging system property: + * + * -Djavax.net.debug=all + * + * This gives a lot of low-level information about operations underway, + * including specific handshake messages, and might be best examined + * after gaining some familiarity with this application. + */ + private static boolean debug = false; + + private SSLContext sslc; + + private SSLEngine clientEngine; // client Engine + private ByteBuffer clientOut; // write side of clientEngine + private ByteBuffer clientIn; // read side of clientEngine + + private SSLEngine serverEngine; // server Engine + private ByteBuffer serverOut; // write side of serverEngine + private ByteBuffer serverIn; // read side of serverEngine + + /* + * For data transport, this example uses local ByteBuffers. This + * isn't really useful, but the purpose of this example is to show + * SSLEngine concepts, not how to do network transport. + */ + private ByteBuffer cTOs; // "reliable" transport client->server + private ByteBuffer sTOc; // "reliable" transport server->client + + /* + * The following is to set up the keystores. + */ + private static String keyStoreFile = "testkeys"; + private static String trustStoreFile = "testkeys"; + private static String passwd = "passphrase"; + + /* + * Main entry point for this demo. + */ + public static void main(String args[]) throws Exception { + if (debug) { + System.setProperty("javax.net.debug", "all"); + } + + EngineTest demo = new EngineTest(); + demo.runDemo(); + + System.out.println("Demo Completed."); + } + + /* + * Create an initialized SSLContext to use for this demo. + */ + public EngineTest() throws Exception { + + KeyStore ks = KeyStore.getInstance("JKS"); + KeyStore ts = KeyStore.getInstance("JKS"); + + char[] passphrase = "passphrase".toCharArray(); + + // ks.load(new FileInputStream(keyStoreFile), passphrase); + // ts.load(new FileInputStream(trustStoreFile), passphrase); + + ks.load(null, null); + ts.load(null, null); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ts); + + SSLContext sslCtx = SSLContext.getInstance("TLS"); + + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + sslc = sslCtx; + } + + /* + * Run the demo. + * + * Sit in a tight loop, both engines calling wrap/unwrap regardless + * of whether data is available or not. We do this until both engines + * report back they are closed. + * + * The main loop handles all of the I/O phases of the SSLEngine's + * lifetime: + * + * initial handshaking + * application data transfer + * engine closing + * + * One could easily separate these phases into separate + * sections of code. + */ + private void runDemo() throws Exception { + boolean dataDone = false; + + createSSLEngines(); + createBuffers(); + + SSLEngineResult clientResult; // results from client's last operation + SSLEngineResult serverResult; // results from server's last operation + + /* + * Examining the SSLEngineResults could be much more involved, + * and may alter the overall flow of the application. + * + * For example, if we received a BUFFER_OVERFLOW when trying + * to write to the output pipe, we could reallocate a larger + * pipe, but instead we wait for the peer to drain it. + */ + while (!isEngineClosed(clientEngine) || + !isEngineClosed(serverEngine)) { + + log("================"); + + clientResult = clientEngine.wrap(clientOut, cTOs); + log("client wrap: ", clientResult); + runDelegatedTasks(clientResult, clientEngine); + + serverResult = serverEngine.wrap(serverOut, sTOc); + log("server wrap: ", serverResult); + runDelegatedTasks(serverResult, serverEngine); + + cTOs.flip(); + sTOc.flip(); + + log("----"); + + clientResult = clientEngine.unwrap(sTOc, clientIn); + log("client unwrap: ", clientResult); + runDelegatedTasks(clientResult, clientEngine); + + serverResult = serverEngine.unwrap(cTOs, serverIn); + log("server unwrap: ", serverResult); + runDelegatedTasks(serverResult, serverEngine); + + cTOs.compact(); + sTOc.compact(); + + /* + * After we've transfered all application data between the client + * and server, we close the clientEngine's outbound stream. + * This generates a close_notify handshake message, which the + * server engine receives and responds by closing itself. + * + * In normal operation, each SSLEngine should call + * closeOutbound(). To protect against truncation attacks, + * SSLEngine.closeInbound() should be called whenever it has + * determined that no more input data will ever be + * available (say a closed input stream). + */ + if (!dataDone && (clientOut.limit() == serverIn.position()) && + (serverOut.limit() == clientIn.position())) { + + /* + * A sanity check to ensure we got what was sent. + */ + checkTransfer(serverOut, clientIn); + checkTransfer(clientOut, serverIn); + + log("\tClosing clientEngine's *OUTBOUND*..."); + clientEngine.closeOutbound(); + // serverEngine.closeOutbound(); + dataDone = true; + } + } + } + + /* + * Using the SSLContext created during object creation, + * create/configure the SSLEngines we'll use for this demo. + */ + private void createSSLEngines() throws Exception { + /* + * Configure the serverEngine to act as a server in the SSL/TLS + * handshake. Also, require SSL client authentication. + */ + serverEngine = sslc.createSSLEngine(); + serverEngine.setUseClientMode(false); + serverEngine.setNeedClientAuth(true); + + /* + * Similar to above, but using client mode instead. + */ + clientEngine = sslc.createSSLEngine("client", 80); + clientEngine.setUseClientMode(true); + } + + /* + * Create and size the buffers appropriately. + */ + private void createBuffers() { + + /* + * We'll assume the buffer sizes are the same + * between client and server. + */ + SSLSession session = clientEngine.getSession(); + int appBufferMax = session.getApplicationBufferSize(); + int netBufferMax = session.getPacketBufferSize(); + + /* + * We'll make the input buffers a bit bigger than the max needed + * size, so that unwrap()s following a successful data transfer + * won't generate BUFFER_OVERFLOWS. + * + * We'll use a mix of direct and indirect ByteBuffers for + * tutorial purposes only. In reality, only use direct + * ByteBuffers when they give a clear performance enhancement. + */ + clientIn = ByteBuffer.allocate(appBufferMax + 50); + serverIn = ByteBuffer.allocate(appBufferMax + 50); + + cTOs = ByteBuffer.allocateDirect(netBufferMax); + sTOc = ByteBuffer.allocateDirect(netBufferMax); + + clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes()); + serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes()); + } + + /* + * If the result indicates that we have outstanding tasks to do, + * go ahead and run them in this thread. + */ + private static void runDelegatedTasks(SSLEngineResult result, + SSLEngine engine) throws Exception { + + if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable; + while ((runnable = engine.getDelegatedTask()) != null) { + log("\trunning delegated task..."); + runnable.run(); + } + HandshakeStatus hsStatus = engine.getHandshakeStatus(); + if (hsStatus == HandshakeStatus.NEED_TASK) { + throw new Exception( + "handshake shouldn't need additional tasks"); + } + log("\tnew HandshakeStatus: " + hsStatus); + } + } + + private static boolean isEngineClosed(SSLEngine engine) { + return (engine.isOutboundDone() && engine.isInboundDone()); + } + + /* + * Simple check to make sure everything came across as expected. + */ + private static void checkTransfer(ByteBuffer a, ByteBuffer b) + throws Exception { + a.flip(); + b.flip(); + + if (!a.equals(b)) { + throw new Exception("Data didn't transfer cleanly"); + } else { + log("\tData transferred cleanly"); + } + + a.position(a.limit()); + b.position(b.limit()); + a.limit(a.capacity()); + b.limit(b.capacity()); + } + + /* + * Logging code + */ + private static boolean resultOnce = true; + + private static void log(String str, SSLEngineResult result) { + if (!logging) { + return; + } + if (resultOnce) { + resultOnce = false; + System.out.println("The format of the SSLEngineResult is: \n" + + "\t\"getStatus() / getHandshakeStatus()\" +\n" + + "\t\"bytesConsumed() / bytesProduced()\"\n"); + } + HandshakeStatus hsStatus = result.getHandshakeStatus(); + log(str + + result.getStatus() + "/" + hsStatus + ", " + + result.bytesConsumed() + "/" + result.bytesProduced() + + " bytes"); + if (hsStatus == HandshakeStatus.FINISHED) { + log("\t...ready for application data"); + } + } + + private static void log(String str) { + if (logging) { + System.out.println(str); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/aliyun/gmsse/GMSSLContextSpiTest.java b/src/test/java/com/aliyun/gmsse/GMSSLContextSpiTest.java index 8b92ded..479abce 100644 --- a/src/test/java/com/aliyun/gmsse/GMSSLContextSpiTest.java +++ b/src/test/java/com/aliyun/gmsse/GMSSLContextSpiTest.java @@ -21,11 +21,11 @@ public void nullTest() throws Exception { GMSSLContextSpi mySSLContextSpi = new GMSSLContextSpi(); Method engineCreateSSLEngine = GMSSLContextSpi.class.getDeclaredMethod("engineCreateSSLEngine"); engineCreateSSLEngine.setAccessible(true); - Assert.assertNull(engineCreateSSLEngine.invoke(mySSLContextSpi)); + Assert.assertNotNull(engineCreateSSLEngine.invoke(mySSLContextSpi)); Method engineCreateSSLEngineMethod = GMSSLContextSpi.class.getDeclaredMethod("engineCreateSSLEngine", String.class, int.class); engineCreateSSLEngineMethod.setAccessible(true); - Assert.assertNull(engineCreateSSLEngineMethod.invoke(mySSLContextSpi, null, 0)); + Assert.assertNotNull(engineCreateSSLEngineMethod.invoke(mySSLContextSpi, null, 0)); Method engineGetServerSessionContext = GMSSLContextSpi.class.getDeclaredMethod("engineGetServerSessionContext"); engineGetServerSessionContext.setAccessible(true); diff --git a/src/test/java/com/aliyun/gmsse/MainTest.java b/src/test/java/com/aliyun/gmsse/MainTest.java index b64d492..87c25e4 100644 --- a/src/test/java/com/aliyun/gmsse/MainTest.java +++ b/src/test/java/com/aliyun/gmsse/MainTest.java @@ -1,30 +1,261 @@ package com.aliyun.gmsse; import java.io.BufferedReader; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.nio.channels.ReadableByteChannel; + import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.UnrecoverableKeyException; +import java.util.Iterator; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; + import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import java.nio.channels.WritableByteChannel; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.Assert; import org.junit.Test; public class MainTest { + + public abstract class SSLProvider implements Runnable { + final SSLEngine engine; + final Executor ioWorker, taskWorkers; + final ByteBuffer clientWrap, clientUnwrap; + final ByteBuffer serverWrap, serverUnwrap; + + public SSLProvider(SSLEngine engine, int capacity, Executor ioWorker, Executor taskWorkers) { + this.clientWrap = ByteBuffer.allocate(capacity); + this.serverWrap = ByteBuffer.allocate(capacity); + this.clientUnwrap = ByteBuffer.allocate(capacity); + this.serverUnwrap = ByteBuffer.allocate(capacity); + this.clientUnwrap.limit(0); + this.engine = engine; + this.ioWorker = ioWorker; + this.taskWorkers = taskWorkers; + this.ioWorker.execute(this); + } + + public abstract void onInput(ByteBuffer decrypted); + + public abstract void onOutput(ByteBuffer encrypted); + + public abstract void onFailure(Exception ex); + + public abstract void onSuccess(); + + public abstract void onClosed(); + + public void sendAsync(final ByteBuffer data) { + this.ioWorker.execute(new Runnable() { + @Override + public void run() { + clientWrap.put(data); + + SSLProvider.this.run(); + } + }); + } + + public void notify(final ByteBuffer data) { + this.ioWorker.execute(new Runnable() { + @Override + public void run() { + clientUnwrap.put(data); + SSLProvider.this.run(); + } + }); + } + + public void run() { + // executes non-blocking tasks on the IO-Worker + while (this.isHandShaking()) { + continue; + } + } + + private synchronized boolean isHandShaking() { + switch (engine.getHandshakeStatus()) { + case NOT_HANDSHAKING: + boolean occupied = false; { + if (clientWrap.position() > 0) + occupied |= this.wrap(); + if (clientUnwrap.position() > 0) + occupied |= this.unwrap(); + } + return occupied; + + case NEED_WRAP: + if (!this.wrap()) + return false; + break; + + case NEED_UNWRAP: + if (!this.unwrap()) + return false; + break; + + case NEED_TASK: + final Runnable sslTask = engine.getDelegatedTask(); + Runnable wrappedTask = new Runnable() { + @Override + public void run() { + sslTask.run(); + ioWorker.execute(SSLProvider.this); + } + }; + taskWorkers.execute(wrappedTask); + return false; + + case FINISHED: + throw new IllegalStateException("FINISHED"); + } + + return true; + } + + private boolean wrap() { + SSLEngineResult wrapResult; + + try { + clientWrap.flip(); + wrapResult = engine.wrap(clientWrap, serverWrap); + clientWrap.compact(); + } catch (SSLException exc) { + this.onFailure(exc); + return false; + } + + switch (wrapResult.getStatus()) { + case OK: + if (serverWrap.position() > 0) { + serverWrap.flip(); + this.onOutput(serverWrap); + serverWrap.compact(); + } + break; + + case BUFFER_UNDERFLOW: + // try again later + break; + + case BUFFER_OVERFLOW: + throw new IllegalStateException("failed to wrap"); + + case CLOSED: + this.onClosed(); + return false; + } + + return true; + } + + private boolean unwrap() { + SSLEngineResult unwrapResult; + + try { + clientUnwrap.flip(); + unwrapResult = engine.unwrap(clientUnwrap, serverUnwrap); + clientUnwrap.compact(); + } catch (SSLException ex) { + this.onFailure(ex); + return false; + } + + switch (unwrapResult.getStatus()) { + case OK: + if (serverUnwrap.position() > 0) { + serverUnwrap.flip(); + this.onInput(serverUnwrap); + serverUnwrap.compact(); + } + break; + + case CLOSED: + this.onClosed(); + return false; + + case BUFFER_OVERFLOW: + throw new IllegalStateException("failed to unwrap"); + + case BUFFER_UNDERFLOW: + return false; + } + + if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { + this.onSuccess(); + return false; + } + + return true; + } + } + + public abstract class NioSSLProvider extends SSLProvider { + private final ByteBuffer buffer = ByteBuffer.allocate(32 * 1024); + private final SelectionKey key; + + public NioSSLProvider(SelectionKey key, SSLEngine engine, int bufferSize, Executor ioWorker, + Executor taskWorkers) { + super(engine, bufferSize, ioWorker, taskWorkers); + this.key = key; + } + + @Override + public void onOutput(ByteBuffer encrypted) { + try { + ((WritableByteChannel) this.key.channel()).write(encrypted); + } catch (IOException exc) { + throw new IllegalStateException(exc); + } + } + + public boolean processInput() { + buffer.clear(); + int bytes; + try { + bytes = ((ReadableByteChannel) this.key.channel()).read(buffer); + } catch (IOException ex) { + bytes = -1; + } + if (bytes == -1) { + return false; + } + buffer.flip(); + ByteBuffer copy = ByteBuffer.allocate(bytes); + copy.put(buffer); + copy.flip(); + this.notify(copy); + return true; + } + } + @Test public void test() throws IOException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, UnrecoverableKeyException, URISyntaxException { GMProvider provider = new GMProvider(); @@ -34,17 +265,13 @@ public void test() throws IOException, NoSuchAlgorithmException, KeyManagementEx X509Certificate cert = Helper.loadCertificate("WoTrus-SM2.crt"); KeyStore ks = KeyStore.getInstance("JKS"); ks.load(null, null); - ks.setCertificateEntry("gmca", cert); + ks.setCertificateEntry("alias", cert); // init trust manager factory TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509", provider); tmf.init(ks); - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, null); - KeyManager[] kms = kmf.getKeyManagers(); - - sc.init(kms, tmf.getTrustManagers(), null); + sc.init(null, tmf.getTrustManagers(), null); SSLSocketFactory ssf = sc.getSocketFactory(); @@ -66,4 +293,145 @@ public void test() throws IOException, NoSuchAlgorithmException, KeyManagementEx Assert.assertTrue(buffer.toString().contains("沃通")); connInputStream.close(); } + + @Test + public void testEngine() throws IOException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException, + CertificateException { + GMProvider provider = new GMProvider(); + SSLContext sc = SSLContext.getInstance("TLS", provider); + + // 信任管理器 + BouncyCastleProvider bc = new BouncyCastleProvider(); + KeyStore ks = KeyStore.getInstance("JKS"); + CertificateFactory cf = CertificateFactory.getInstance("X.509", bc); + InputStream is = this.getClass().getClassLoader().getResourceAsStream("WoTrus-SM2.crt"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(is); + ks.load(null, null); + ks.setCertificateEntry("alias", cert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509", provider); + tmf.init(ks); + + sc.init(null, tmf.getTrustManagers(), null); + + SSLEngine engine = sc.createSSLEngine(); + engine.setUseClientMode(true); + + InetSocketAddress address = new InetSocketAddress("sm2only.ovssl.cn", 443); + SocketChannel channel = SocketChannel.open(); + channel.configureBlocking(false); + channel.connect(address); + + engine.beginHandshake(); + + Selector selector = Selector.open(); + + int ops = SelectionKey.OP_CONNECT | SelectionKey.OP_READ; + + SelectionKey key = channel.register(selector, ops); + + // create the worker threads + final Executor ioWorker = Executors.newSingleThreadExecutor(); + final Executor taskWorkers = Executors.newFixedThreadPool(2); + + final int ioBufferSize = 32 * 1024; + final NioSSLProvider ssl = new NioSSLProvider(key, engine, ioBufferSize, ioWorker, taskWorkers) { + @Override + public void onFailure(Exception ex) { + System.out.println("handshake failure"); + ex.printStackTrace(); + } + + @Override + public void onSuccess() { + System.out.println("handshake success"); + SSLSession session = engine.getSession(); + try { + System.out.println("local principal: " + session.getLocalPrincipal()); + System.out.println("remote principal: " + session.getPeerPrincipal()); + System.out.println("cipher: " + session.getCipherSuite()); + } catch (Exception exc) { + exc.printStackTrace(); + } + + // HTTP request + StringBuilder http = new StringBuilder(); + http.append("GET / HTTP/1.0\r\n"); + http.append("Connection: close\r\n"); + http.append("\r\n"); + byte[] data = http.toString().getBytes(); + ByteBuffer send = ByteBuffer.wrap(data); + this.sendAsync(send); + } + + @Override + public void onInput(ByteBuffer decrypted) { + // HTTP response + byte[] dst = new byte[decrypted.remaining()]; + decrypted.get(dst); + String response = new String(dst); + System.out.print(response); + System.out.flush(); + } + + @Override + public void onClosed() { + System.out.println("ssl session closed"); + } + }; + + // NIO selector + while (true) { + key.selector().select(); + Iterator keys = key.selector().selectedKeys().iterator(); + while (keys.hasNext()) { + keys.next(); + keys.remove(); + ssl.processInput(); + } + } + } + + @Test + public void testEngine2() throws Exception { + // GMProvider provider = new GMProvider(); + // SSLContext sc = SSLContext.getInstance("TLS", provider); + + // X509Certificate cert = Helper.loadCertificate("WoTrus-SM2.crt"); + // KeyStore ks = KeyStore.getInstance("JKS"); + // ks.load(null, null); + // ks.setCertificateEntry("alias", cert); + + // TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509", provider); + // tmf.init(ks); + + // sc.init(null, tmf.getTrustManagers(), null); + + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, null, null); + SSLEngine engine = sc.createSSLEngine("sm2only.ovssl.cn", 443); + engine.setUseClientMode(true); + SSLSession session = engine.getSession(); + int appBufferMax = session.getApplicationBufferSize(); + int netBufferMax = session.getPacketBufferSize(); + ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax + 50); + ByteBuffer c2s = ByteBuffer.allocateDirect(netBufferMax); + SSLEngineResult clientResult; + while (!(engine.isOutboundDone() && engine.isInboundDone())) { + clientResult = engine.wrap(ByteBuffer.wrap("Hi Server, I'm Client".getBytes()), c2s); + if (clientResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable; + while ((runnable = engine.getDelegatedTask()) != null) { + runnable.run(); + } + HandshakeStatus hsStatus = engine.getHandshakeStatus(); + if (hsStatus == HandshakeStatus.NEED_TASK) { + throw new Exception("handshake shouldn't need additional tasks"); + } + } + c2s.flip(); + // clientResult = engine.unwrap(s2c, clientIn); + c2s.compact(); + } + } }