From 248cf5c96db7136465d818ebf325dcea530b699f Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Mon, 20 Mar 2017 10:11:56 +0000 Subject: [PATCH] HTTPCLIENT-1834: NTLN auth refactoring; support for MS CredSsp auth Contributed by Radovan Semancik and Karl Wright git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1787707 13f79535-47bb-0310-9956-ffa450edef68 --- .../hc/client5/http/config/AuthSchemes.java | 5 + .../client5/http/impl/auth/CredSspScheme.java | 1134 +++++++++++++++++ .../http/impl/auth/CredSspSchemeFactory.java | 42 + .../hc/client5/http/impl/auth/DebugUtil.java | 95 ++ .../http/impl/auth/NTLMEngineImpl.java | 913 ++++++++++--- .../DefaultAuthenticationStrategy.java | 1 + .../http/impl/sync/HttpClientBuilder.java | 4 +- .../http/impl/auth/TestNTLMEngineImpl.java | 96 +- 8 files changed, 2077 insertions(+), 213 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspScheme.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspSchemeFactory.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DebugUtil.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/config/AuthSchemes.java b/httpclient5/src/main/java/org/apache/hc/client5/http/config/AuthSchemes.java index a33454cb0..f18d40408 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/config/AuthSchemes.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/config/AuthSchemes.java @@ -69,6 +69,11 @@ public final class AuthSchemes { */ public static final String KERBEROS = "Kerberos"; + /** + * CredSSP authentication scheme defined in [MS-CSSP]. + */ + public static final String CREDSSP = "CredSSP"; + private AuthSchemes() { } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspScheme.java new file mode 100644 index 000000000..6a511d029 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspScheme.java @@ -0,0 +1,1134 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.auth; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.commons.codec.binary.Base64; +import org.apache.hc.client5.http.auth.AuthChallenge; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.AuthenticationException; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; +import org.apache.hc.client5.http.auth.NTCredentials; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Args; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + *

+ * Client implementation of the CredSSP protocol specified in [MS-CSSP]. + *

+ *

+ * Note: This is implementation is NOT GSS based. It should be. But there is no Java NTLM + * implementation as GSS module. Maybe the NTLMEngine can be converted to GSS and then this + * can be also switched to GSS. In fact it only works in CredSSP+NTLM case. + *

+ *

+ * Based on [MS-CSSP]: Credential Security Support Provider (CredSSP) Protocol (Revision 13.0, 7/14/2016). + * The implementation was inspired by Python CredSSP and NTLM implementation by Jordan Borean. + *

+ */ +public class CredSspScheme implements AuthScheme +{ + private static final Charset UNICODE_LITTLE_UNMARKED = Charset.forName( "UnicodeLittleUnmarked" ); + public static final String SCHEME_NAME = "CredSSP"; + + private final Logger log = LogManager.getLogger( CredSspScheme.class ); + + enum State + { + // Nothing sent, nothing received + UNINITIATED, + + // We are handshaking. Several messages are exchanged in this state + TLS_HANDSHAKE, + + // TLS handshake finished. Channel established + TLS_HANDSHAKE_FINISHED, + + // NTLM NEGOTIATE message sent (strictly speaking this should be SPNEGO) + NEGO_TOKEN_SENT, + + // NTLM CHALLENGE message received (strictly speaking this should be SPNEGO) + NEGO_TOKEN_RECEIVED, + + // NTLM AUTHENTICATE message sent together with a server public key + PUB_KEY_AUTH_SENT, + + // Server public key authentication message received + PUB_KEY_AUTH_RECEIVED, + + // Credentials message sent. Protocol exchange finished. + CREDENTIALS_SENT; + } + + private State state; + private SSLEngine sslEngine; + private NTCredentials ntcredentials; + private NTLMEngineImpl.Type1Message type1Message; + private NTLMEngineImpl.Type2Message type2Message; + private NTLMEngineImpl.Type3Message type3Message; + private CredSspTsRequest lastReceivedTsRequest; + private NTLMEngineImpl.Handle ntlmOutgoingHandle; + private NTLMEngineImpl.Handle ntlmIncomingHandle; + private byte[] peerPublicKey; + + + public CredSspScheme() { + state = State.UNINITIATED; + } + + + @Override + public String getName() + { + return SCHEME_NAME; + } + + + @Override + public String getRealm() + { + return null; + } + + + @Override + public boolean isConnectionBased() + { + return true; + } + + + private SSLEngine getSSLEngine() + { + if ( sslEngine == null ) + { + sslEngine = createSSLEngine(); + } + return sslEngine; + } + + + private SSLEngine createSSLEngine() + { + final SSLContext sslContext; + try + { + sslContext = SSLContexts.custom().build(); + } + catch ( NoSuchAlgorithmException | KeyManagementException e ) + { + throw new RuntimeException( "Error creating SSL Context: " + e.getMessage(), e ); + } + + final X509TrustManager tm = new X509TrustManager() + { + + @Override + public void checkClientTrusted( final X509Certificate[] chain, final String authType ) + throws CertificateException + { + // Nothing to do. + } + + + @Override + public void checkServerTrusted( final X509Certificate[] chain, final String authType ) + throws CertificateException + { + // Nothing to do, accept all. CredSSP server is using its own certificate without any + // binding to the PKI trust chains. The public key is verified as part of the CredSSP + // protocol exchange. + } + + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return null; + } + + }; + try + { + sslContext.init( null, new TrustManager[] + { tm }, null ); + } + catch ( KeyManagementException e ) + { + throw new RuntimeException( "SSL Context initialization error: " + e.getMessage(), e ); + } + final SSLEngine sslEngine = sslContext.createSSLEngine(); + sslEngine.setUseClientMode( true ); + return sslEngine; + } + + + @Override + public void processChallenge( + final AuthChallenge authChallenge, + final HttpContext context) throws MalformedChallengeException + { + final String inputString = authChallenge.getValue(); + + if ( inputString.isEmpty() ) + { + if ( state == State.UNINITIATED ) + { + // This is OK, just send out first message. That should start TLS handshake + } + else + { + final String msg = "Received unexpected empty input in state " + state; + log.error( msg ); + throw new MalformedChallengeException( msg ); + } + } + + if ( state == State.TLS_HANDSHAKE ) + { + unwrapHandshake( inputString ); + if ( getSSLEngine().getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING ) + { + log.trace( "TLS handshake finished" ); + state = State.TLS_HANDSHAKE_FINISHED; + } + } + + if ( state == State.NEGO_TOKEN_SENT ) + { + final ByteBuffer buf = unwrap( inputString ); + state = State.NEGO_TOKEN_RECEIVED; + lastReceivedTsRequest = CredSspTsRequest.createDecoded( buf ); + } + + if ( state == State.PUB_KEY_AUTH_SENT ) + { + final ByteBuffer buf = unwrap( inputString ); + state = State.PUB_KEY_AUTH_RECEIVED; + lastReceivedTsRequest = CredSspTsRequest.createDecoded( buf ); + } + } + + @Override + public boolean isResponseReady( + final HttpHost host, + final CredentialsProvider credentialsProvider, + final HttpContext context) throws AuthenticationException { + Args.notNull(host, "Auth host"); + Args.notNull(credentialsProvider, "CredentialsProvider"); + + final Credentials credentials = credentialsProvider.getCredentials( + new AuthScope(host, null, getName()), context); + if (credentials instanceof NTCredentials) { + this.ntcredentials = (NTCredentials) credentials; + return true; + } + return false; + } + + @Override + public Principal getPrincipal() { + return ntcredentials != null ? ntcredentials.getUserPrincipal() : null; + } + + + @Override + public String generateAuthResponse( + final HttpHost host, + final HttpRequest request, + final HttpContext context) throws AuthenticationException + { + if (ntcredentials == null) { + throw new AuthenticationException("NT credentials not available"); + } + + final String outputString; + + if ( state == State.UNINITIATED ) + { + beginTlsHandshake(); + outputString = wrapHandshake(); + state = State.TLS_HANDSHAKE; + + } + else if ( state == State.TLS_HANDSHAKE ) + { + outputString = wrapHandshake(); + + } + else if ( state == State.TLS_HANDSHAKE_FINISHED ) + { + + final int ntlmFlags = getNtlmFlags(); + final ByteBuffer buf = allocateOutBuffer(); + type1Message = new NTLMEngineImpl.Type1Message( + ntcredentials.getDomain(), ntcredentials.getWorkstation(), ntlmFlags); + final byte[] ntlmNegoMessageEncoded = type1Message.getBytes(); + final CredSspTsRequest req = CredSspTsRequest.createNegoToken( ntlmNegoMessageEncoded ); + req.encode( buf ); + buf.flip(); + outputString = wrap( buf ); + state = State.NEGO_TOKEN_SENT; + + } + else if ( state == State.NEGO_TOKEN_RECEIVED ) + { + final ByteBuffer buf = allocateOutBuffer(); + type2Message = new NTLMEngineImpl.Type2Message( + lastReceivedTsRequest.getNegoToken()); + + final Certificate peerServerCertificate = getPeerServerCertificate(); + + type3Message = new NTLMEngineImpl.Type3Message( + ntcredentials.getDomain(), + ntcredentials.getWorkstation(), + ntcredentials.getUserName(), + ntcredentials.getPassword(), + type2Message.getChallenge(), + type2Message.getFlags(), + type2Message.getTarget(), + type2Message.getTargetInfo(), + peerServerCertificate, + type1Message.getBytes(), + type2Message.getBytes()); + + final byte[] ntlmAuthenticateMessageEncoded = type3Message.getBytes(); + + final byte[] exportedSessionKey = type3Message.getExportedSessionKey(); + + ntlmOutgoingHandle = new NTLMEngineImpl.Handle(exportedSessionKey, NTLMEngineImpl.Mode.CLIENT, true); + ntlmIncomingHandle = new NTLMEngineImpl.Handle(exportedSessionKey, NTLMEngineImpl.Mode.SERVER, true); + + final CredSspTsRequest req = CredSspTsRequest.createNegoToken( ntlmAuthenticateMessageEncoded ); + peerPublicKey = getSubjectPublicKeyDer( peerServerCertificate.getPublicKey() ); + final byte[] pubKeyAuth = createPubKeyAuth(); + req.setPubKeyAuth( pubKeyAuth ); + + req.encode( buf ); + buf.flip(); + outputString = wrap( buf ); + state = State.PUB_KEY_AUTH_SENT; + + } + else if ( state == State.PUB_KEY_AUTH_RECEIVED ) + { + verifyPubKeyAuthResponse( lastReceivedTsRequest.getPubKeyAuth() ); + final byte[] authInfo = createAuthInfo( ntcredentials ); + final CredSspTsRequest req = CredSspTsRequest.createAuthInfo( authInfo ); + + final ByteBuffer buf = allocateOutBuffer(); + req.encode( buf ); + buf.flip(); + outputString = wrap( buf ); + state = State.CREDENTIALS_SENT; + } + else + { + throw new AuthenticationException( "Wrong state " + state ); + } + return outputString; + } + + + private int getNtlmFlags() + { + return NTLMEngineImpl.FLAG_REQUEST_OEM_ENCODING | + NTLMEngineImpl.FLAG_REQUEST_SIGN | + NTLMEngineImpl.FLAG_REQUEST_SEAL | + NTLMEngineImpl.FLAG_DOMAIN_PRESENT | + NTLMEngineImpl.FLAG_REQUEST_ALWAYS_SIGN | + NTLMEngineImpl.FLAG_REQUEST_NTLM2_SESSION | + NTLMEngineImpl.FLAG_TARGETINFO_PRESENT | + NTLMEngineImpl.FLAG_REQUEST_VERSION | + NTLMEngineImpl.FLAG_REQUEST_128BIT_KEY_EXCH | + NTLMEngineImpl.FLAG_REQUEST_EXPLICIT_KEY_EXCH | + NTLMEngineImpl.FLAG_REQUEST_56BIT_ENCRYPTION; + } + + + private Certificate getPeerServerCertificate() throws AuthenticationException + { + final Certificate[] peerCertificates; + try + { + peerCertificates = sslEngine.getSession().getPeerCertificates(); + } + catch ( SSLPeerUnverifiedException e ) + { + throw new AuthenticationException( e.getMessage(), e ); + } + for ( Certificate peerCertificate : peerCertificates ) + { + if ( !( peerCertificate instanceof X509Certificate ) ) + { + continue; + } + final X509Certificate peerX509Cerificate = ( X509Certificate ) peerCertificate; + if ( peerX509Cerificate.getBasicConstraints() != -1 ) + { + continue; + } + return peerX509Cerificate; + } + return null; + } + + + private byte[] createPubKeyAuth() throws AuthenticationException + { + return ntlmOutgoingHandle.signAndEncryptMessage( peerPublicKey ); + } + + + private void verifyPubKeyAuthResponse( final byte[] pubKeyAuthResponse ) throws AuthenticationException + { + final byte[] pubKeyReceived = ntlmIncomingHandle.decryptAndVerifySignedMessage( pubKeyAuthResponse ); + + // assert: pubKeyReceived = peerPublicKey + 1 + // The following algorithm is a bit simplified. But due to the ASN.1 encoding the first byte + // of the public key will be 0x30 we can pretty much rely on a fact that there will be no carry + if ( peerPublicKey.length != pubKeyReceived.length ) + { + throw new AuthenticationException( "Public key mismatch in pubKeyAuth response" ); + } + if ( ( peerPublicKey[0] + 1 ) != pubKeyReceived[0] ) + { + throw new AuthenticationException( "Public key mismatch in pubKeyAuth response" ); + } + for ( int i = 1; i < peerPublicKey.length; i++ ) + { + if ( peerPublicKey[i] != pubKeyReceived[i] ) + { + throw new AuthenticationException( "Public key mismatch in pubKeyAuth response" ); + } + } + log.trace( "Received public key response is valid" ); + } + + + private byte[] createAuthInfo( final NTCredentials ntcredentials ) throws AuthenticationException + { + + final byte[] domainBytes = encodeUnicode( ntcredentials.getDomain() ); + final byte[] domainOctetStringBytesLengthBytes = encodeLength( domainBytes.length ); + final int domainNameLength = 1 + domainOctetStringBytesLengthBytes.length + domainBytes.length; + final byte[] domainNameLengthBytes = encodeLength( domainNameLength ); + + final byte[] usernameBytes = encodeUnicode( ntcredentials.getUserName() ); + final byte[] usernameOctetStringBytesLengthBytes = encodeLength( usernameBytes.length ); + final int userNameLength = 1 + usernameOctetStringBytesLengthBytes.length + usernameBytes.length; + final byte[] userNameLengthBytes = encodeLength( userNameLength ); + + final byte[] passwordBytes = encodeUnicode( ntcredentials.getPassword() ); + final byte[] passwordOctetStringBytesLengthBytes = encodeLength( passwordBytes.length ); + final int passwordLength = 1 + passwordOctetStringBytesLengthBytes.length + passwordBytes.length; + final byte[] passwordLengthBytes = encodeLength( passwordLength ); + + final int tsPasswordLength = 1 + domainNameLengthBytes.length + domainNameLength + + 1 + userNameLengthBytes.length + userNameLength + + 1 + passwordLengthBytes.length + passwordLength; + final byte[] tsPasswordLengthBytes = encodeLength( tsPasswordLength ); + final int credentialsOctetStringLength = 1 + tsPasswordLengthBytes.length + tsPasswordLength; + final byte[] credentialsOctetStringLengthBytes = encodeLength( credentialsOctetStringLength ); + final int credentialsLength = 1 + credentialsOctetStringLengthBytes.length + credentialsOctetStringLength; + final byte[] credentialsLengthBytes = encodeLength( credentialsLength ); + final int tsCredentialsLength = 5 + 1 + credentialsLengthBytes.length + credentialsLength; + final byte[] tsCredentialsLengthBytes = encodeLength( tsCredentialsLength ); + + final ByteBuffer buf = ByteBuffer.allocate( 1 + tsCredentialsLengthBytes.length + tsCredentialsLength ); + + // TSCredentials structure [MS-CSSP] section 2.2.1.2 + buf.put( ( byte ) 0x30 ); // seq + buf.put( tsCredentialsLengthBytes ); + + buf.put( ( byte ) ( 0x00 | 0xa0 ) ); // credType tag [0] + buf.put( ( byte ) 3 ); // credType length + buf.put( ( byte ) 0x02 ); // type: INTEGER + buf.put( ( byte ) 1 ); // credType inner length + buf.put( ( byte ) 1 ); // credType value: 1 (password) + + buf.put( ( byte ) ( 0x01 | 0xa0 ) ); // credentials tag [1] + buf.put( credentialsLengthBytes ); + buf.put( ( byte ) 0x04 ); // type: OCTET STRING + buf.put( credentialsOctetStringLengthBytes ); + + // TSPasswordCreds structure [MS-CSSP] section 2.2.1.2.1 + buf.put( ( byte ) 0x30 ); // seq + buf.put( tsPasswordLengthBytes ); + + buf.put( ( byte ) ( 0x00 | 0xa0 ) ); // domainName tag [0] + buf.put( domainNameLengthBytes ); + buf.put( ( byte ) 0x04 ); // type: OCTET STRING + buf.put( domainOctetStringBytesLengthBytes ); + buf.put( domainBytes ); + + buf.put( ( byte ) ( 0x01 | 0xa0 ) ); // userName tag [1] + buf.put( userNameLengthBytes ); + buf.put( ( byte ) 0x04 ); // type: OCTET STRING + buf.put( usernameOctetStringBytesLengthBytes ); + buf.put( usernameBytes ); + + buf.put( ( byte ) ( 0x02 | 0xa0 ) ); // password tag [2] + buf.put( passwordLengthBytes ); + buf.put( ( byte ) 0x04 ); // type: OCTET STRING + buf.put( passwordOctetStringBytesLengthBytes ); + buf.put( passwordBytes ); + + final byte[] authInfo = buf.array(); + try + { + return ntlmOutgoingHandle.signAndEncryptMessage( authInfo ); + } + catch ( NTLMEngineException e ) + { + throw new AuthenticationException( e.getMessage(), e ); + } + } + + private final static byte[] EMPTYBUFFER = new byte[0]; + + private byte[] encodeUnicode( final String string ) + { + if (string == null) { + return EMPTYBUFFER; + } + return encodeUnicode( CharBuffer.wrap(string) ); + } + + + private byte[] encodeUnicode( final char[] chars ) + { + if (chars == null) { + return EMPTYBUFFER; + } + return encodeUnicode( CharBuffer.wrap(chars) ); + } + + + private byte[] encodeUnicode( final CharBuffer charBuffer ) + { + if (charBuffer == null) { + return EMPTYBUFFER; + } + final ByteBuffer encoded = UNICODE_LITTLE_UNMARKED.encode(charBuffer); + if (!encoded.hasRemaining()) { + return EMPTYBUFFER; + } + final byte[] bytes = new byte[encoded.remaining()]; + encoded.get(bytes); + return bytes; + } + + private byte[] getSubjectPublicKeyDer( final PublicKey publicKey ) throws AuthenticationException + { + // The publicKey.getEncoded() returns encoded SubjectPublicKeyInfo structure. But the CredSSP expects + // SubjectPublicKey subfield. I have found no easy way how to get just the SubjectPublicKey from + // java.security libraries. So let's use a primitive way and parse it out from the DER. + + try + { + final byte[] encodedPubKeyInfo = publicKey.getEncoded(); + + final ByteBuffer buf = ByteBuffer.wrap( encodedPubKeyInfo ); + getByteAndAssert( buf, 0x30, "initial sequence" ); + parseLength( buf ); + getByteAndAssert( buf, 0x30, "AlgorithmIdentifier sequence" ); + final int algIdSeqLength = parseLength( buf ); + buf.position( buf.position() + algIdSeqLength ); + getByteAndAssert( buf, 0x03, "subjectPublicKey type" ); + int subjectPublicKeyLegth = parseLength( buf ); + // There may be leading padding byte ... or whatever that is. Skip that. + final byte b = buf.get(); + if ( b == 0 ) + { + subjectPublicKeyLegth--; + } + else + { + buf.position( buf.position() - 1 ); + } + final byte[] subjectPublicKey = new byte[subjectPublicKeyLegth]; + buf.get( subjectPublicKey ); + return subjectPublicKey; + } + catch ( MalformedChallengeException e ) + { + throw new AuthenticationException( e.getMessage(), e ); + } + } + + + private void beginTlsHandshake() throws AuthenticationException + { + try + { + getSSLEngine().beginHandshake(); + } + catch ( SSLException e ) + { + throw new AuthenticationException( "SSL Engine error: " + e.getMessage(), e ); + } + } + + + private ByteBuffer allocateOutBuffer() + { + final SSLEngine sslEngine = getSSLEngine(); + final SSLSession sslSession = sslEngine.getSession(); + return ByteBuffer.allocate( sslSession.getApplicationBufferSize() ); + } + + + private String wrapHandshake() throws AuthenticationException + { + final ByteBuffer src = allocateOutBuffer(); + src.flip(); + final SSLEngine sslEngine = getSSLEngine(); + final SSLSession sslSession = sslEngine.getSession(); + // Needs to be twice the size as there may be two wraps during handshake. + // Primitive and inefficient solution, but it works. + final ByteBuffer dst = ByteBuffer.allocate( sslSession.getPacketBufferSize() * 2 ); + while ( sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP ) + { + wrap( src, dst ); + } + dst.flip(); + return encodeBase64( dst ); + } + + + private String wrap( final ByteBuffer src ) throws AuthenticationException + { + final SSLEngine sslEngine = getSSLEngine(); + final SSLSession sslSession = sslEngine.getSession(); + final ByteBuffer dst = ByteBuffer.allocate( sslSession.getPacketBufferSize() ); + wrap( src, dst ); + dst.flip(); + return encodeBase64( dst ); + } + + + private void wrap( final ByteBuffer src, final ByteBuffer dst ) throws AuthenticationException + { + final SSLEngine sslEngine = getSSLEngine(); + try + { + final SSLEngineResult engineResult = sslEngine.wrap( src, dst ); + if ( engineResult.getStatus() != Status.OK ) + { + throw new AuthenticationException( "SSL Engine error status: " + engineResult.getStatus() ); + } + } + catch ( SSLException e ) + { + throw new AuthenticationException( "SSL Engine wrap error: " + e.getMessage(), e ); + } + } + + + private void unwrapHandshake( final String inputString ) throws MalformedChallengeException + { + final SSLEngine sslEngine = getSSLEngine(); + final SSLSession sslSession = sslEngine.getSession(); + final ByteBuffer src = decodeBase64( inputString ); + final ByteBuffer dst = ByteBuffer.allocate( sslSession.getApplicationBufferSize() ); + while ( sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP ) + { + unwrap( src, dst ); + } + } + + + private ByteBuffer unwrap( final String inputString ) throws MalformedChallengeException + { + final SSLEngine sslEngine = getSSLEngine(); + final SSLSession sslSession = sslEngine.getSession(); + final ByteBuffer src = decodeBase64( inputString ); + final ByteBuffer dst = ByteBuffer.allocate( sslSession.getApplicationBufferSize() ); + unwrap( src, dst ); + dst.flip(); + return dst; + } + + + private void unwrap( final ByteBuffer src, final ByteBuffer dst ) throws MalformedChallengeException + { + + try + { + final SSLEngineResult engineResult = sslEngine.unwrap( src, dst ); + if ( engineResult.getStatus() != Status.OK ) + { + throw new MalformedChallengeException( "SSL Engine error status: " + engineResult.getStatus() ); + } + + if ( sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_TASK ) + { + final Runnable task = sslEngine.getDelegatedTask(); + task.run(); + } + + } + catch ( SSLException e ) + { + throw new MalformedChallengeException( "SSL Engine unwrap error: " + e.getMessage(), e ); + } + } + + + private String encodeBase64( final ByteBuffer buffer ) + { + final int limit = buffer.limit(); + final byte[] bytes = new byte[limit]; + buffer.get( bytes ); + return new String(Base64.encodeBase64(bytes), StandardCharsets.US_ASCII); + } + + + private ByteBuffer decodeBase64( final String inputString ) + { + final byte[] inputBytes = Base64.decodeBase64(inputString.getBytes(StandardCharsets.US_ASCII)); + final ByteBuffer buffer = ByteBuffer.wrap( inputBytes ); + return buffer; + } + + + @Override + public boolean isChallengeComplete() + { + return state == State.CREDENTIALS_SENT; + } + + /** + * Implementation of the TsRequest structure used in CredSSP protocol. + * It is specified in [MS-CPPS] section 2.2.1. + */ + static class CredSspTsRequest + { + + private static final int VERSION = 3; + + private byte[] negoToken; + private byte[] authInfo; + private byte[] pubKeyAuth; + + + protected CredSspTsRequest() + { + super(); + } + + + public static CredSspTsRequest createNegoToken( final byte[] negoToken ) + { + final CredSspTsRequest req = new CredSspTsRequest(); + req.negoToken = negoToken; + return req; + } + + + public static CredSspTsRequest createAuthInfo( final byte[] authInfo ) + { + final CredSspTsRequest req = new CredSspTsRequest(); + req.authInfo = authInfo; + return req; + } + + + public static CredSspTsRequest createDecoded( final ByteBuffer buf ) throws MalformedChallengeException + { + final CredSspTsRequest req = new CredSspTsRequest(); + req.decode( buf ); + return req; + } + + + public byte[] getNegoToken() + { + return negoToken; + } + + + public void setNegoToken( final byte[] negoToken ) + { + this.negoToken = negoToken; + } + + + public byte[] getAuthInfo() + { + return authInfo; + } + + + public void setAuthInfo( final byte[] authInfo ) + { + this.authInfo = authInfo; + } + + + public byte[] getPubKeyAuth() + { + return pubKeyAuth; + } + + + public void setPubKeyAuth( final byte[] pubKeyAuth ) + { + this.pubKeyAuth = pubKeyAuth; + } + + + public void decode( final ByteBuffer buf ) throws MalformedChallengeException + { + negoToken = null; + authInfo = null; + pubKeyAuth = null; + + getByteAndAssert( buf, 0x30, "initial sequence" ); + parseLength( buf ); + + while ( buf.hasRemaining() ) + { + final int contentTag = getAndAssertContentSpecificTag( buf, "content tag" ); + parseLength( buf ); + switch ( contentTag ) + { + case 0: + processVersion( buf ); + break; + case 1: + parseNegoTokens( buf ); + break; + case 2: + parseAuthInfo( buf ); + break; + case 3: + parsePubKeyAuth( buf ); + break; + case 4: + processErrorCode( buf ); + break; + default: + parseError( buf, "unexpected content tag " + contentTag ); + } + } + } + + + private void processVersion( final ByteBuffer buf ) throws MalformedChallengeException + { + getByteAndAssert( buf, 0x02, "version type" ); + getLengthAndAssert( buf, 1, "version length" ); + getByteAndAssert( buf, VERSION, "wrong protocol version" ); + } + + + private void parseNegoTokens( final ByteBuffer buf ) throws MalformedChallengeException + { + getByteAndAssert( buf, 0x30, "negoTokens sequence" ); + parseLength( buf ); + // I have seen both 0x30LL encoding and 0x30LL0x30LL encoding. Accept both. + byte bufByte = buf.get(); + if ( bufByte == 0x30 ) + { + parseLength( buf ); + bufByte = buf.get(); + } + if ( ( bufByte & 0xff ) != 0xa0 ) + { + parseError( buf, "negoTokens: wrong content-specific tag " + String.format( "%02X", bufByte ) ); + } + parseLength( buf ); + getByteAndAssert( buf, 0x04, "negoToken type" ); + + final int tokenLength = parseLength( buf ); + negoToken = new byte[tokenLength]; + buf.get( negoToken ); + } + + + private void parseAuthInfo( final ByteBuffer buf ) throws MalformedChallengeException + { + getByteAndAssert( buf, 0x04, "authInfo type" ); + final int length = parseLength( buf ); + authInfo = new byte[length]; + buf.get( authInfo ); + } + + + private void parsePubKeyAuth( final ByteBuffer buf ) throws MalformedChallengeException + { + getByteAndAssert( buf, 0x04, "pubKeyAuth type" ); + final int length = parseLength( buf ); + pubKeyAuth = new byte[length]; + buf.get( pubKeyAuth ); + } + + + private void processErrorCode( final ByteBuffer buf ) throws MalformedChallengeException + { + getLengthAndAssert( buf, 3, "error code length" ); + getByteAndAssert( buf, 0x02, "error code type" ); + getLengthAndAssert( buf, 1, "error code length" ); + final byte errorCode = buf.get(); + parseError( buf, "Error code " + errorCode ); + } + + + public void encode( final ByteBuffer buf ) + { + final ByteBuffer inner = ByteBuffer.allocate( buf.capacity() ); + + // version tag [0] + inner.put( ( byte ) ( 0x00 | 0xa0 ) ); + inner.put( ( byte ) 3 ); // length + + inner.put( ( byte ) ( 0x02 ) ); // INTEGER tag + inner.put( ( byte ) 1 ); // length + inner.put( ( byte ) VERSION ); // value + + if ( negoToken != null ) + { + int len = negoToken.length; + final byte[] negoTokenLengthBytes = encodeLength( len ); + len += 1 + negoTokenLengthBytes.length; + final byte[] negoTokenLength1Bytes = encodeLength( len ); + len += 1 + negoTokenLength1Bytes.length; + final byte[] negoTokenLength2Bytes = encodeLength( len ); + len += 1 + negoTokenLength2Bytes.length; + final byte[] negoTokenLength3Bytes = encodeLength( len ); + len += 1 + negoTokenLength3Bytes.length; + final byte[] negoTokenLength4Bytes = encodeLength( len ); + + inner.put( ( byte ) ( 0x01 | 0xa0 ) ); // negoData tag [1] + inner.put( negoTokenLength4Bytes ); // length + + inner.put( ( byte ) ( 0x30 ) ); // SEQUENCE tag + inner.put( negoTokenLength3Bytes ); // length + + inner.put( ( byte ) ( 0x30 ) ); // .. of SEQUENCE tag + inner.put( negoTokenLength2Bytes ); // length + + inner.put( ( byte ) ( 0x00 | 0xa0 ) ); // negoToken tag [0] + inner.put( negoTokenLength1Bytes ); // length + + inner.put( ( byte ) ( 0x04 ) ); // OCTET STRING tag + inner.put( negoTokenLengthBytes ); // length + + inner.put( negoToken ); + } + + if ( authInfo != null ) + { + final byte[] authInfoEncodedLength = encodeLength( authInfo.length ); + + inner.put( ( byte ) ( 0x02 | 0xa0 ) ); // authInfo tag [2] + inner.put( encodeLength( 1 + authInfoEncodedLength.length + authInfo.length ) ); // length + + inner.put( ( byte ) ( 0x04 ) ); // OCTET STRING tag + inner.put( authInfoEncodedLength ); + inner.put( authInfo ); + } + + if ( pubKeyAuth != null ) + { + final byte[] pubKeyAuthEncodedLength = encodeLength( pubKeyAuth.length ); + + inner.put( ( byte ) ( 0x03 | 0xa0 ) ); // pubKeyAuth tag [3] + inner.put( encodeLength( 1 + pubKeyAuthEncodedLength.length + pubKeyAuth.length ) ); // length + + inner.put( ( byte ) ( 0x04 ) ); // OCTET STRING tag + inner.put( pubKeyAuthEncodedLength ); + inner.put( pubKeyAuth ); + } + + inner.flip(); + + // SEQUENCE tag + buf.put( ( byte ) ( 0x10 | 0x20 ) ); + buf.put( encodeLength( inner.limit() ) ); + buf.put( inner ); + } + + + public String debugDump() + { + final StringBuilder sb = new StringBuilder( "TsRequest\n" ); + sb.append( " negoToken:\n" ); + sb.append( " " ); + DebugUtil.dump( sb, negoToken ); + sb.append( "\n" ); + sb.append( " authInfo:\n" ); + sb.append( " " ); + DebugUtil.dump( sb, authInfo ); + sb.append( "\n" ); + sb.append( " pubKeyAuth:\n" ); + sb.append( " " ); + DebugUtil.dump( sb, pubKeyAuth ); + return sb.toString(); + } + + + @Override + public String toString() + { + return "TsRequest(negoToken=" + Arrays.toString( negoToken ) + ", authInfo=" + + Arrays.toString( authInfo ) + ", pubKeyAuth=" + Arrays.toString( pubKeyAuth ) + ")"; + } + } + + static void getByteAndAssert( final ByteBuffer buf, final int expectedValue, final String errorMessage ) + throws MalformedChallengeException + { + final byte bufByte = buf.get(); + if ( bufByte != expectedValue ) + { + parseError( buf, errorMessage + expectMessage( expectedValue, bufByte ) ); + } + } + + private static String expectMessage( final int expectedValue, final int realValue ) + { + return "(expected " + String.format( "%02X", expectedValue ) + ", got " + String.format( "%02X", realValue ) + + ")"; + } + + static int parseLength( final ByteBuffer buf ) + { + byte bufByte = buf.get(); + if ( bufByte == 0x80 ) + { + return -1; // infinite + } + if ( ( bufByte & 0x80 ) == 0x80 ) + { + final int size = bufByte & 0x7f; + int length = 0; + for ( int i = 0; i < size; i++ ) + { + bufByte = buf.get(); + length = ( length << 8 ) + ( bufByte & 0xff ); + } + return length; + } + else + { + return bufByte; + } + } + + static void getLengthAndAssert( final ByteBuffer buf, final int expectedValue, final String errorMessage ) + throws MalformedChallengeException + { + final int bufLength = parseLength( buf ); + if ( expectedValue != bufLength ) + { + parseError( buf, errorMessage + expectMessage( expectedValue, bufLength ) ); + } + } + + static int getAndAssertContentSpecificTag( final ByteBuffer buf, final String errorMessage ) throws MalformedChallengeException + { + final byte bufByte = buf.get(); + if ( ( bufByte & 0xe0 ) != 0xa0 ) + { + parseError( buf, errorMessage + ": wrong content-specific tag " + String.format( "%02X", bufByte ) ); + } + final int tag = bufByte & 0x1f; + return tag; + } + + static void parseError( final ByteBuffer buf, final String errorMessage ) throws MalformedChallengeException + { + throw new MalformedChallengeException( + "Error parsing TsRequest (position:" + buf.position() + "): " + errorMessage ); + } + + static byte[] encodeLength( final int length ) + { + if ( length < 128 ) + { + final byte[] encoded = new byte[1]; + encoded[0] = ( byte ) length; + return encoded; + } + + int size = 1; + + int val = length; + while ( ( val >>>= 8 ) != 0 ) + { + size++; + } + + final byte[] encoded = new byte[1 + size]; + encoded[0] = ( byte ) ( size | 0x80 ); + + int shift = ( size - 1 ) * 8; + for ( int i = 0; i < size; i++ ) + { + encoded[i + 1] = ( byte ) ( length >> shift ); + shift -= 8; + } + + return encoded; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspSchemeFactory.java new file mode 100644 index 000000000..fd28b6c34 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/CredSspSchemeFactory.java @@ -0,0 +1,42 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.auth; + +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeProvider; +import org.apache.hc.core5.http.protocol.HttpContext; + +public class CredSspSchemeFactory implements AuthSchemeProvider +{ + + @Override + public AuthScheme create(final HttpContext context) { + return new CredSspScheme(); + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DebugUtil.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DebugUtil.java new file mode 100644 index 000000000..e7e8a53e5 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DebugUtil.java @@ -0,0 +1,95 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.auth; + + +import java.nio.ByteBuffer; + +/** + * Simple debugging utility class for CredSSP and NTLM implementations. + */ +class DebugUtil +{ + + public static String dump( final ByteBuffer buf ) + { + final ByteBuffer dup = buf.duplicate(); + final StringBuilder sb = new StringBuilder( dup.toString() ); + sb.append( ": " ); + while ( dup.position() < dup.limit() ) + { + sb.append( String.format( "%02X ", dup.get() ) ); + } + return sb.toString(); + } + + + public static void dump( final StringBuilder sb, final byte[] bytes ) + { + if ( bytes == null ) + { + sb.append( "null" ); + return; + } + for ( byte b : bytes ) + { + sb.append( String.format( "%02X ", b ) ); + } + } + + + public static String dump( final byte[] bytes ) + { + final StringBuilder sb = new StringBuilder(); + dump( sb, bytes ); + return sb.toString(); + } + + + public static byte[] fromHex( final String hex ) + { + int i = 0; + final byte[] bytes = new byte[200000]; + int h = 0; + while ( h < hex.length() ) + { + if ( hex.charAt( h ) == ' ' ) + { + h++; + } + final String str = hex.substring( h, h + 2 ); + bytes[i] = ( byte ) Integer.parseInt( str, 16 ); + i++; + h = h + 2; + } + final byte[] outbytes = new byte[i]; + System.arraycopy( bytes, 0, outbytes, 0, i ); + return outbytes; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java index 6b114ba75..7efa882f0 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMEngineImpl.java @@ -26,13 +26,16 @@ */ package org.apache.hc.client5.http.impl.auth; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.util.Arrays; import java.util.Locale; +import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; @@ -57,22 +60,41 @@ final class NTLMEngineImpl implements NTLMEngine { // http://davenport.sourceforge.net/ntlm.html // and // http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx - protected static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested - protected static final int FLAG_REQUEST_TARGET = 0x00000004; // Requests target field - protected static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message. - protected static final int FLAG_REQUEST_SEAL = 0x00000020; // Request key exchange for message confidentiality in NEGOTIATE message. MUST be used in conjunction with 56BIT. - protected static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key - protected static final int FLAG_REQUEST_NTLMv1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both - protected static final int FLAG_DOMAIN_PRESENT = 0x00001000; // Domain is present in message - protected static final int FLAG_WORKSTATION_PRESENT = 0x00002000; // Workstation is present in message - protected static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL. - protected static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security - protected static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version - protected static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present - protected static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange - protected static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange - protected static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL + // [MS-NLMP] section 2.2.2.5 + static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested + static final int FLAG_REQUEST_OEM_ENCODING = 0x00000002; // OEM string encoding requested + static final int FLAG_REQUEST_TARGET = 0x00000004; // Requests target field + static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message. + static final int FLAG_REQUEST_SEAL = 0x00000020; // Request key exchange for message confidentiality in NEGOTIATE message. MUST be used in conjunction with 56BIT. + static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key + static final int FLAG_REQUEST_NTLMv1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both + static final int FLAG_DOMAIN_PRESENT = 0x00001000; // Domain is present in message + static final int FLAG_WORKSTATION_PRESENT = 0x00002000; // Workstation is present in message + static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL. + static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security + static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version + static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present + static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange + static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange + static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL + // Attribute-value identifiers (AvId) + // according to [MS-NLMP] section 2.2.2.1 + static final int MSV_AV_EOL = 0x0000; // Indicates that this is the last AV_PAIR in the list. + static final int MSV_AV_NB_COMPUTER_NAME = 0x0001; // The server's NetBIOS computer name. + static final int MSV_AV_NB_DOMAIN_NAME = 0x0002; // The server's NetBIOS domain name. + static final int MSV_AV_DNS_COMPUTER_NAME = 0x0003; // The fully qualified domain name (FQDN) of the computer. + static final int MSV_AV_DNS_DOMAIN_NAME = 0x0004; // The FQDN of the domain. + static final int MSV_AV_DNS_TREE_NAME = 0x0005; // The FQDN of the forest. + static final int MSV_AV_FLAGS = 0x0006; // A 32-bit value indicating server or client configuration. + static final int MSV_AV_TIMESTAMP = 0x0007; // server local time + static final int MSV_AV_SINGLE_HOST = 0x0008; // A Single_Host_Data structure. + static final int MSV_AV_TARGET_NAME = 0x0009; // The SPN of the target server. + static final int MSV_AV_CHANNEL_BINDINGS = 0x000A; // A channel bindings hash. + + static final int MSV_AV_FLAGS_ACCOUNT_AUTH_CONSTAINED = 0x00000001; // Indicates to the client that the account authentication is constrained. + static final int MSV_AV_FLAGS_MIC = 0x00000002; // Indicates that the client is providing message integrity in the MIC field in the AUTHENTICATE_MESSAGE. + static final int MSV_AV_FLAGS_UNTRUSTED_TARGET_SPN = 0x00000004; // Indicates that the client is providing a target SPN generated from an untrusted source. /** Secure random generator */ private static final java.security.SecureRandom RND_GEN; @@ -86,17 +108,36 @@ final class NTLMEngineImpl implements NTLMEngine { } /** The signature string as bytes in the default encoding */ - private static final byte[] SIGNATURE; + private static final byte[] SIGNATURE = getNullTerminatedAsciiString("NTLMSSP"); - static { - final byte[] bytesWithoutNull = "NTLMSSP".getBytes(StandardCharsets.US_ASCII); - SIGNATURE = new byte[bytesWithoutNull.length + 1]; - System.arraycopy(bytesWithoutNull, 0, SIGNATURE, 0, bytesWithoutNull.length); - SIGNATURE[bytesWithoutNull.length] = (byte) 0x00; + // Key derivation magic strings for the SIGNKEY algorithm defined in + // [MS-NLMP] section 3.4.5.2ASCII + private static final byte[] SIGN_MAGIC_SERVER = getNullTerminatedAsciiString( + "session key to server-to-client signing key magic constant"); + private static final byte[] SIGN_MAGIC_CLIENT = getNullTerminatedAsciiString( + "session key to client-to-server signing key magic constant"); + private static final byte[] SEAL_MAGIC_SERVER = getNullTerminatedAsciiString( + "session key to server-to-client sealing key magic constant"); + private static final byte[] SEAL_MAGIC_CLIENT = getNullTerminatedAsciiString( + "session key to client-to-server sealing key magic constant"); + + // prefix for GSS API channel binding + private static final byte[] MAGIC_TLS_SERVER_ENDPOINT = "tls-server-end-point:".getBytes(StandardCharsets.US_ASCII); + + private static byte[] getNullTerminatedAsciiString( final String source ) + { + final byte[] bytesWithoutNull = source.getBytes(StandardCharsets.US_ASCII); + final byte[] target = new byte[bytesWithoutNull.length + 1]; + System.arraycopy(bytesWithoutNull, 0, target, 0, bytesWithoutNull.length); + target[bytesWithoutNull.length] = (byte) 0x00; + return target; } private static final String TYPE_1_MESSAGE = new Type1Message().getResponse(); + NTLMEngineImpl() { + } + /** * Returns the response for the given message. * @@ -111,8 +152,6 @@ final class NTLMEngineImpl implements NTLMEngine { * @param domain * the NT domain to authenticate in. * @return The response. - * @throws org.apache.hc.core5.http.HttpException - * If the messages cannot be retrieved. */ static String getResponseFor(final String message, final String username, final char[] password, final String host, final String domain) throws NTLMEngineException { @@ -122,8 +161,39 @@ final class NTLMEngineImpl implements NTLMEngine { response = getType1Message(host, domain); } else { final Type2Message t2m = new Type2Message(message); - response = getType3Message(username, password, host, domain, t2m.getChallenge(), t2m - .getFlags(), t2m.getTarget(), t2m.getTargetInfo()); + response = getType3Message(username, password, host, domain, t2m.getChallenge(), + t2m.getFlags(), t2m.getTarget(), t2m.getTargetInfo()); + } + return response; + } + + /** + * Returns the response for the given message. + * + * @param message + * the message that was received from the server. + * @param username + * the username to authenticate with. + * @param password + * the password to authenticate with. + * @param host + * The host. + * @param domain + * the NT domain to authenticate in. + * @return The response. + */ + static String getResponseFor(final String message, final String username, final char[] password, + final String host, final String domain, final Certificate peerServerCertificate) throws NTLMEngineException { + + final String response; + if (message == null || message.trim().equals("")) { + response = new Type1Message(host, domain).getResponse(); + } else { + final Type1Message t1m = new Type1Message(host, domain); + final Type2Message t2m = new Type2Message(message); + response = getType3Message(username, password, host, domain, t2m.getChallenge(), + t2m.getFlags(), t2m.getTarget(), t2m.getTargetInfo(), + peerServerCertificate, t1m.getBytes(), t2m.getBytes()); } return response; } @@ -172,6 +242,32 @@ final class NTLMEngineImpl implements NTLMEngine { targetInformation).getResponse(); } + /** + * Creates the type 3 message using the given server nonce. The type 3 + * message includes all the information for authentication, host, domain, + * username and the result of encrypting the nonce sent by the server using + * the user's password as the key. + * + * @param user + * The user name. This should not include the domain name. + * @param password + * The password. + * @param host + * The host that is originating the authentication request. + * @param domain + * The domain to authenticate within. + * @param nonce + * the 8 byte array the server sent. + * @return The type 3 message. + */ + static String getType3Message(final String user, final char[] password, final String host, final String domain, + final byte[] nonce, final int type2Flags, final String target, final byte[] targetInformation, + final Certificate peerServerCertificate, final byte[] type1Message, final byte[] type2Message) + throws NTLMEngineException { + return new Type3Message(domain, host, user, password, nonce, type2Flags, target, + targetInformation, peerServerCertificate, type1Message, type2Message).getResponse(); + } + private static int readULong(final byte[] src, final int index) throws NTLMEngineException { if (src.length < index + 4) { throw new NTLMEngineException("NTLM authentication - buffer too small for DWORD"); @@ -200,66 +296,67 @@ final class NTLMEngineImpl implements NTLMEngine { } /** Calculate a challenge block */ - private static byte[] makeRandomChallenge() throws NTLMEngineException { - if (RND_GEN == null) { - throw new NTLMEngineException("Random generator not available"); - } + private static byte[] makeRandomChallenge(final Random random) throws NTLMEngineException { final byte[] rval = new byte[8]; - synchronized (RND_GEN) { - RND_GEN.nextBytes(rval); + synchronized (random) { + random.nextBytes(rval); } return rval; } /** Calculate a 16-byte secondary key */ - private static byte[] makeSecondaryKey() throws NTLMEngineException { - if (RND_GEN == null) { - throw new NTLMEngineException("Random generator not available"); - } + private static byte[] makeSecondaryKey(final Random random) throws NTLMEngineException { final byte[] rval = new byte[16]; - synchronized (RND_GEN) { - RND_GEN.nextBytes(rval); + synchronized (random) { + random.nextBytes(rval); } return rval; } - protected static class CipherGen { + static class CipherGen { - protected final String domain; - protected final String user; - protected final char[] password; - protected final byte[] challenge; - protected final String target; - protected final byte[] targetInformation; + final Random random; + final long currentTime; + + final String domain; + final String user; + final char[] password; + final byte[] challenge; + final String target; + final byte[] targetInformation; // Information we can generate but may be passed in (for testing) - protected byte[] clientChallenge; - protected byte[] clientChallenge2; - protected byte[] secondaryKey; - protected byte[] timestamp; + byte[] clientChallenge; + byte[] clientChallenge2; + byte[] secondaryKey; + byte[] timestamp; // Stuff we always generate - protected byte[] lmHash = null; - protected byte[] lmResponse = null; - protected byte[] ntlmHash = null; - protected byte[] ntlmResponse = null; - protected byte[] ntlmv2Hash = null; - protected byte[] lmv2Hash = null; - protected byte[] lmv2Response = null; - protected byte[] ntlmv2Blob = null; - protected byte[] ntlmv2Response = null; - protected byte[] ntlm2SessionResponse = null; - protected byte[] lm2SessionResponse = null; - protected byte[] lmUserSessionKey = null; - protected byte[] ntlmUserSessionKey = null; - protected byte[] ntlmv2UserSessionKey = null; - protected byte[] ntlm2SessionResponseUserSessionKey = null; - protected byte[] lanManagerSessionKey = null; + byte[] lmHash = null; + byte[] lmResponse = null; + byte[] ntlmHash = null; + byte[] ntlmResponse = null; + byte[] ntlmv2Hash = null; + byte[] lmv2Hash = null; + byte[] lmv2Response = null; + byte[] ntlmv2Blob = null; + byte[] ntlmv2Response = null; + byte[] ntlm2SessionResponse = null; + byte[] lm2SessionResponse = null; + byte[] lmUserSessionKey = null; + byte[] ntlmUserSessionKey = null; + byte[] ntlmv2UserSessionKey = null; + byte[] ntlm2SessionResponseUserSessionKey = null; + byte[] lanManagerSessionKey = null; - public CipherGen(final String domain, final String user, final char[] password, + public CipherGen(final Random random, final long currentTime, + final String domain, final String user, final char[] password, final byte[] challenge, final String target, final byte[] targetInformation, final byte[] clientChallenge, final byte[] clientChallenge2, final byte[] secondaryKey, final byte[] timestamp) { + this.random = random; + this.currentTime = currentTime; + this.domain = domain; this.target = target; this.user = user; @@ -272,16 +369,21 @@ final class NTLMEngineImpl implements NTLMEngine { this.timestamp = timestamp; } - public CipherGen(final String domain, final String user, final char[] password, - final byte[] challenge, final String target, final byte[] targetInformation) { - this(domain, user, password, challenge, target, targetInformation, null, null, null, null); + public CipherGen(final Random random, final long currentTime, + final String domain, + final String user, + final char[] password, + final byte[] challenge, + final String target, + final byte[] targetInformation) { + this(random, currentTime, domain, user, password, challenge, target, targetInformation, null, null, null, null); } /** Calculate and return client challenge */ public byte[] getClientChallenge() throws NTLMEngineException { if (clientChallenge == null) { - clientChallenge = makeRandomChallenge(); + clientChallenge = makeRandomChallenge(random); } return clientChallenge; } @@ -290,7 +392,7 @@ final class NTLMEngineImpl implements NTLMEngine { public byte[] getClientChallenge2() throws NTLMEngineException { if (clientChallenge2 == null) { - clientChallenge2 = makeRandomChallenge(); + clientChallenge2 = makeRandomChallenge(random); } return clientChallenge2; } @@ -299,7 +401,7 @@ final class NTLMEngineImpl implements NTLMEngine { public byte[] getSecondaryKey() throws NTLMEngineException { if (secondaryKey == null) { - secondaryKey = makeSecondaryKey(); + secondaryKey = makeSecondaryKey(random); } return secondaryKey; } @@ -361,8 +463,8 @@ final class NTLMEngineImpl implements NTLMEngine { /** Calculate a timestamp */ public byte[] getTimestamp() { if (timestamp == null) { - long time = System.currentTimeMillis(); - time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch. + long time = this.currentTime; + time += 11644473600000L; // milliseconds from January 1, 1601 -> epoch. time *= 10000; // tenths of a microsecond. // convert to little-endian byte array. timestamp = new byte[8]; @@ -529,7 +631,7 @@ final class NTLMEngineImpl implements NTLMEngine { static byte[] ntlm2SessionResponse(final byte[] ntlmHash, final byte[] challenge, final byte[] clientChallenge) throws NTLMEngineException { try { - final MessageDigest md5 = MessageDigest.getInstance("MD5"); + final MessageDigest md5 = getMD5(); md5.update(challenge); md5.update(clientChallenge); final byte[] digest = md5.digest(); @@ -701,6 +803,188 @@ final class NTLMEngineImpl implements NTLMEngine { return lmv2Response; } + enum Mode + { + CLIENT, SERVER; + } + + static class Handle + { + private final byte[] signingKey; + private byte[] sealingKey; + private final Cipher rc4; + final Mode mode; + final private boolean isConnection; + int sequenceNumber = 0; + + + Handle( final byte[] exportedSessionKey, final Mode mode, final boolean isConnection ) throws NTLMEngineException + { + this.isConnection = isConnection; + this.mode = mode; + try + { + final MessageDigest signMd5 = getMD5(); + final MessageDigest sealMd5 = getMD5(); + signMd5.update( exportedSessionKey ); + sealMd5.update( exportedSessionKey ); + if ( mode == Mode.CLIENT ) + { + signMd5.update( SIGN_MAGIC_CLIENT ); + sealMd5.update( SEAL_MAGIC_CLIENT ); + } + else + { + signMd5.update( SIGN_MAGIC_SERVER ); + sealMd5.update( SEAL_MAGIC_SERVER ); + } + signingKey = signMd5.digest(); + sealingKey = sealMd5.digest(); + } + catch ( final Exception e ) + { + throw new NTLMEngineException( e.getMessage(), e ); + } + rc4 = initCipher(); + } + + public byte[] getSigningKey() + { + return signingKey; + } + + + public byte[] getSealingKey() + { + return sealingKey; + } + + private Cipher initCipher() throws NTLMEngineException + { + final Cipher cipher; + try + { + cipher = Cipher.getInstance( "RC4" ); + if ( mode == Mode.CLIENT ) + { + cipher.init( Cipher.ENCRYPT_MODE, new SecretKeySpec( sealingKey, "RC4" ) ); + } + else + { + cipher.init( Cipher.DECRYPT_MODE, new SecretKeySpec( sealingKey, "RC4" ) ); + } + } + catch ( Exception e ) + { + throw new NTLMEngineException( e.getMessage(), e ); + } + return cipher; + } + + + private void advanceMessageSequence() throws NTLMEngineException + { + if ( !isConnection ) + { + final MessageDigest sealMd5 = getMD5(); + sealMd5.update( sealingKey ); + final byte[] seqNumBytes = new byte[4]; + writeULong( seqNumBytes, sequenceNumber, 0 ); + sealMd5.update( seqNumBytes ); + sealingKey = sealMd5.digest(); + initCipher(); + } + sequenceNumber++; + } + + private byte[] encrypt( final byte[] data ) throws NTLMEngineException + { + return rc4.update( data ); + } + + private byte[] decrypt( final byte[] data ) throws NTLMEngineException + { + return rc4.update( data ); + } + + private byte[] computeSignature( final byte[] message ) throws NTLMEngineException + { + final byte[] sig = new byte[16]; + + // version + sig[0] = 0x01; + sig[1] = 0x00; + sig[2] = 0x00; + sig[3] = 0x00; + + // HMAC (first 8 bytes) + final HMACMD5 hmacMD5 = new HMACMD5( signingKey ); + hmacMD5.update( encodeLong( sequenceNumber ) ); + hmacMD5.update( message ); + final byte[] hmac = hmacMD5.getOutput(); + final byte[] trimmedHmac = new byte[8]; + System.arraycopy( hmac, 0, trimmedHmac, 0, 8 ); + final byte[] encryptedHmac = encrypt( trimmedHmac ); + System.arraycopy( encryptedHmac, 0, sig, 4, 8 ); + + // sequence number + encodeLong( sig, 12, sequenceNumber ); + + return sig; + } + + private boolean validateSignature( final byte[] signature, final byte message[] ) throws NTLMEngineException + { + final byte[] computedSignature = computeSignature( message ); + // log.info( "SSSSS validateSignature("+seqNumber+")\n" + // + " received: " + DebugUtil.dump( signature ) + "\n" + // + " computed: " + DebugUtil.dump( computedSignature ) ); + return Arrays.equals( signature, computedSignature ); + } + + public byte[] signAndEncryptMessage( final byte[] cleartextMessage ) throws NTLMEngineException + { + final byte[] encryptedMessage = encrypt( cleartextMessage ); + final byte[] signature = computeSignature( cleartextMessage ); + final byte[] outMessage = new byte[signature.length + encryptedMessage.length]; + System.arraycopy( signature, 0, outMessage, 0, signature.length ); + System.arraycopy( encryptedMessage, 0, outMessage, signature.length, encryptedMessage.length ); + advanceMessageSequence(); + return outMessage; + } + + public byte[] decryptAndVerifySignedMessage( final byte[] inMessage ) throws NTLMEngineException + { + final byte[] signature = new byte[16]; + System.arraycopy( inMessage, 0, signature, 0, signature.length ); + final byte[] encryptedMessage = new byte[inMessage.length - 16]; + System.arraycopy( inMessage, 16, encryptedMessage, 0, encryptedMessage.length ); + final byte[] cleartextMessage = decrypt( encryptedMessage ); + if ( !validateSignature( signature, cleartextMessage ) ) + { + throw new NTLMEngineException( "Wrong signature" ); + } + advanceMessageSequence(); + return cleartextMessage; + } + + } + + private static byte[] encodeLong( final int value ) + { + final byte[] enc = new byte[4]; + encodeLong( enc, 0, value ); + return enc; + } + + private static void encodeLong( final byte[] buf, final int offset, final int value ) + { + buf[offset + 0] = ( byte ) ( value & 0xff ); + buf[offset + 1] = ( byte ) ( value >> 8 & 0xff ); + buf[offset + 2] = ( byte ) ( value >> 16 & 0xff ); + buf[offset + 3] = ( byte ) ( value >> 24 & 0xff ); + } + /** * Creates the NTLMv2 blob from the given target information block and * client challenge. @@ -784,21 +1068,65 @@ final class NTLMEngineImpl implements NTLMEngine { } } + /** + * Find the character set based on the flags. + * @param flags is the flags. + * @return the character set. + */ + private static Charset getCharset(final int flags) throws NTLMEngineException + { + if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0) { + return DEFAULT_CHARSET; + } else { + if (UNICODE_LITTLE_UNMARKED == null) { + throw new NTLMEngineException( "Unicode not supported" ); + } + return UNICODE_LITTLE_UNMARKED; + } + } + + /** Strip dot suffix from a name */ + private static String stripDotSuffix(final String value) { + if (value == null) { + return null; + } + final int index = value.indexOf("."); + if (index != -1) { + return value.substring(0, index); + } + return value; + } + + /** Convert host to standard form */ + private static String convertHost(final String host) { + return stripDotSuffix(host); + } + + /** Convert domain to standard form */ + private static String convertDomain(final String domain) { + return stripDotSuffix(domain); + } + /** NTLM message generation, base class */ static class NTLMMessage { /** The current response */ - private byte[] messageContents = null; + byte[] messageContents = null; /** The current output position */ - private int currentOutputPosition = 0; + int currentOutputPosition = 0; /** Constructor to use when message contents are not yet known */ NTLMMessage() { } - /** Constructor to use when message contents are known */ + /** Constructor taking a string */ NTLMMessage(final String messageBody, final int expectedType) throws NTLMEngineException { - messageContents = Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET)); + this(Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET)), expectedType); + } + + /** Constructor to use when message bytes are known */ + NTLMMessage(final byte[] message, final int expectedType) throws NTLMEngineException { + messageContents = message; // Look for NTLM message if (messageContents.length < SIGNATURE.length) { throw new NTLMEngineException("NTLM message decoding error - packet too short"); @@ -826,17 +1154,17 @@ final class NTLMEngineImpl implements NTLMEngine { * Get the length of the signature and flags, so calculations can adjust * offsets accordingly. */ - protected int getPreambleLength() { + int getPreambleLength() { return SIGNATURE.length + 4; } /** Get the message length */ - protected int getMessageLength() { + int getMessageLength() { return currentOutputPosition; } /** Read a byte from a position within the message buffer */ - protected byte readByte(final int position) throws NTLMEngineException { + byte readByte(final int position) throws NTLMEngineException { if (messageContents.length < position + 1) { throw new NTLMEngineException("NTLM: Message too short"); } @@ -844,7 +1172,7 @@ final class NTLMEngineImpl implements NTLMEngine { } /** Read a bunch of bytes from a position in the message buffer */ - protected void readBytes(final byte[] buffer, final int position) throws NTLMEngineException { + void readBytes(final byte[] buffer, final int position) throws NTLMEngineException { if (messageContents.length < position + buffer.length) { throw new NTLMEngineException("NTLM: Message too short"); } @@ -852,17 +1180,17 @@ final class NTLMEngineImpl implements NTLMEngine { } /** Read a ushort from a position within the message buffer */ - protected int readUShort(final int position) throws NTLMEngineException { + int readUShort(final int position) throws NTLMEngineException { return NTLMEngineImpl.readUShort(messageContents, position); } /** Read a ulong from a position within the message buffer */ - protected int readULong(final int position) throws NTLMEngineException { + int readULong(final int position) throws NTLMEngineException { return NTLMEngineImpl.readULong(messageContents, position); } /** Read a security buffer from a position within the message buffer */ - protected byte[] readSecurityBuffer(final int position) throws NTLMEngineException { + byte[] readSecurityBuffer(final int position) throws NTLMEngineException { return NTLMEngineImpl.readSecurityBuffer(messageContents, position); } @@ -870,11 +1198,11 @@ final class NTLMEngineImpl implements NTLMEngine { * Prepares the object to create a response of the given length. * * @param maxlength - * the maximum length of the response to prepare, not + * the maximum length of the response to prepare, * including the type and the signature (which this method * adds). */ - protected void prepareResponse(final int maxlength, final int messageType) { + void prepareResponse(final int maxlength, final int messageType) { messageContents = new byte[maxlength]; currentOutputPosition = 0; addBytes(SIGNATURE); @@ -887,7 +1215,7 @@ final class NTLMEngineImpl implements NTLMEngine { * @param b * the byte to add. */ - protected void addByte(final byte b) { + void addByte(final byte b) { messageContents[currentOutputPosition] = b; currentOutputPosition++; } @@ -898,7 +1226,7 @@ final class NTLMEngineImpl implements NTLMEngine { * @param bytes * the bytes to add. */ - protected void addBytes(final byte[] bytes) { + void addBytes(final byte[] bytes) { if (bytes == null) { return; } @@ -909,13 +1237,13 @@ final class NTLMEngineImpl implements NTLMEngine { } /** Adds a USHORT to the response */ - protected void addUShort(final int value) { + void addUShort(final int value) { addByte((byte) (value & 0xff)); addByte((byte) (value >> 8 & 0xff)); } /** Adds a ULong to the response */ - protected void addULong(final int value) { + void addULong(final int value) { addByte((byte) (value & 0xff)); addByte((byte) (value >> 8 & 0xff)); addByte((byte) (value >> 16 & 0xff)); @@ -928,18 +1256,26 @@ final class NTLMEngineImpl implements NTLMEngine { * * @return The response as above. */ - String getResponse() { - final byte[] resp; - if (messageContents.length > currentOutputPosition) { - final byte[] tmp = new byte[currentOutputPosition]; - System.arraycopy(messageContents, 0, tmp, 0, currentOutputPosition); - resp = tmp; - } else { - resp = messageContents; - } - return new String(Base64.encodeBase64(resp), StandardCharsets.US_ASCII); + public String getResponse() { + return new String(Base64.encodeBase64(getBytes()), StandardCharsets.US_ASCII); } + public byte[] getBytes() { + if (messageContents == null) { + buildMessage(); + } + final byte[] resp; + if ( messageContents.length > currentOutputPosition ) { + final byte[] tmp = new byte[currentOutputPosition]; + System.arraycopy( messageContents, 0, tmp, 0, currentOutputPosition ); + messageContents = tmp; + } + return messageContents; + } + + void buildMessage() { + throw new RuntimeException("Message builder not implemented for "+getClass().getName()); + } } /** Type 1 message assembly class */ @@ -947,16 +1283,20 @@ final class NTLMEngineImpl implements NTLMEngine { private final byte[] hostBytes; private final byte[] domainBytes; + private final int flags; Type1Message(final String domain, final String host) throws NTLMEngineException { + this(domain, host, null); + } + + Type1Message(final String domain, final String host, final Integer flags) throws NTLMEngineException { super(); - if (UNICODE_LITTLE_UNMARKED == null) { - throw new NTLMEngineException("Unicode not supported"); - } - // All host name manipulations now take place in the credentials - final String unqualifiedHost = host; - // All domain name manipulations now take place in the credentials - final String unqualifiedDomain = domain; + this.flags = ((flags == null)?getDefaultFlags():flags); + + // Strip off domain name from the host! + final String unqualifiedHost = convertHost(host); + // Use only the base domain name! + final String unqualifiedDomain = convertDomain(domain); hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(UNICODE_LITTLE_UNMARKED) : null; @@ -968,56 +1308,72 @@ final class NTLMEngineImpl implements NTLMEngine { super(); hostBytes = null; domainBytes = null; + flags = getDefaultFlags(); } + + private int getDefaultFlags() { + return + //FLAG_WORKSTATION_PRESENT | + //FLAG_DOMAIN_PRESENT | + + // Required flags + //FLAG_REQUEST_LAN_MANAGER_KEY | + FLAG_REQUEST_NTLMv1 | + FLAG_REQUEST_NTLM2_SESSION | + + // Protocol version request + FLAG_REQUEST_VERSION | + + // Recommended privacy settings + FLAG_REQUEST_ALWAYS_SIGN | + //FLAG_REQUEST_SEAL | + //FLAG_REQUEST_SIGN | + + // These must be set according to documentation, based on use of SEAL above + FLAG_REQUEST_128BIT_KEY_EXCH | + FLAG_REQUEST_56BIT_ENCRYPTION | + //FLAG_REQUEST_EXPLICIT_KEY_EXCH | + + FLAG_REQUEST_UNICODE_ENCODING; + + } + /** * Getting the response involves building the message before returning * it */ @Override - String getResponse() { + void buildMessage() { + int domainBytesLength = 0; + if ( domainBytes != null ) { + domainBytesLength = domainBytes.length; + } + int hostBytesLength = 0; + if ( hostBytes != null ) { + hostBytesLength = hostBytes.length; + } + // Now, build the message. Calculate its length first, including // signature or type. - final int finalLength = 32 + 8 /*+ hostBytes.length + domainBytes.length */; + final int finalLength = 32 + 8 + hostBytesLength + domainBytesLength; // Set up the response. This will initialize the signature, message // type, and flags. prepareResponse(finalLength, 1); // Flags. These are the complete set of flags we support. - addULong( - //FLAG_WORKSTATION_PRESENT | - //FLAG_DOMAIN_PRESENT | - - // Required flags - //FLAG_REQUEST_LAN_MANAGER_KEY | - FLAG_REQUEST_NTLMv1 | - FLAG_REQUEST_NTLM2_SESSION | - - // Protocol version request - FLAG_REQUEST_VERSION | - - // Recommended privacy settings - FLAG_REQUEST_ALWAYS_SIGN | - //FLAG_REQUEST_SEAL | - //FLAG_REQUEST_SIGN | - - // These must be set according to documentation, based on use of SEAL above - FLAG_REQUEST_128BIT_KEY_EXCH | - FLAG_REQUEST_56BIT_ENCRYPTION | - //FLAG_REQUEST_EXPLICIT_KEY_EXCH | - - FLAG_REQUEST_UNICODE_ENCODING); + addULong(flags); // Domain length (two times). - addUShort(/*domainBytes.length*/0); - addUShort(/*domainBytes.length*/0); + addUShort(domainBytesLength); + addUShort(domainBytesLength); // Domain offset. - addULong(/*hostBytes.length +*/ 32 + 8); + addULong(hostBytesLength + 32 + 8); // Host length (two times). - addUShort(/*hostBytes.length*/0); - addUShort(/*hostBytes.length*/0); + addUShort(hostBytesLength); + addUShort(hostBytesLength); // Host offset (always 32 + 8). addULong(32 + 8); @@ -1037,20 +1393,22 @@ final class NTLMEngineImpl implements NTLMEngine { if (domainBytes != null) { addBytes(domainBytes); } - - return super.getResponse(); } } /** Type 2 message class */ static class Type2Message extends NTLMMessage { - protected final byte[] challenge; - protected String target; - protected byte[] targetInfo; - protected final int flags; + final byte[] challenge; + String target; + byte[] targetInfo; + final int flags; - Type2Message(final String message) throws NTLMEngineException { + Type2Message(final String messageBody) throws NTLMEngineException { + this(Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET))); + } + + Type2Message(final byte[] message) throws NTLMEngineException { super(message, 2); // Type 2 message is laid out as follows: @@ -1073,12 +1431,6 @@ final class NTLMEngineImpl implements NTLMEngine { flags = readULong(20); - if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0) { - throw new NTLMEngineException( - "NTLM type 2 message indicates no support for Unicode. Flags are: " - + Integer.toString(flags)); - } - // Do the target! target = null; // The TARGET_DESIRED flag is said to not have understood semantics @@ -1087,11 +1439,7 @@ final class NTLMEngineImpl implements NTLMEngine { if (getMessageLength() >= 12 + 8) { final byte[] bytes = readSecurityBuffer(12); if (bytes.length != 0) { - try { - target = new String(bytes, "UnicodeLittleUnmarked"); - } catch (final UnsupportedEncodingException e) { - throw new NTLMEngineException(e.getMessage(), e); - } + target = new String(bytes, getCharset(flags)); } } @@ -1130,32 +1478,113 @@ final class NTLMEngineImpl implements NTLMEngine { /** Type 3 message assembly class */ static class Type3Message extends NTLMMessage { + // For mic computation + final byte[] type1Message; + final byte[] type2Message; // Response flags from the type2 message - protected final int type2Flags; + final int type2Flags; - protected final byte[] domainBytes; - protected final byte[] hostBytes; - protected final byte[] userBytes; + final byte[] domainBytes; + final byte[] hostBytes; + final byte[] userBytes; - protected byte[] lmResp; - protected byte[] ntResp; - protected final byte[] sessionKey; + byte[] lmResp; + byte[] ntResp; + final byte[] sessionKey; + final byte[] exportedSessionKey; + final boolean computeMic; + + /** More primitive constructor: don't include cert or previous messages. + */ + Type3Message(final String domain, + final String host, + final String user, + final char[] password, + final byte[] nonce, + final int type2Flags, + final String target, + final byte[] targetInformation) + throws NTLMEngineException { + this(domain, host, user, password, nonce, type2Flags, target, targetInformation, null, null, null); + } + + /** More primitive constructor: don't include cert or previous messages. + */ + Type3Message(final Random random, final long currentTime, + final String domain, + final String host, + final String user, + final char[] password, + final byte[] nonce, + final int type2Flags, + final String target, + final byte[] targetInformation) + throws NTLMEngineException { + this(random, currentTime, domain, host, user, password, nonce, type2Flags, target, targetInformation, null, null, null); + } /** Constructor. Pass the arguments we will need */ - Type3Message(final String domain, final String host, final String user, final char[] password, final byte[] nonce, - final int type2Flags, final String target, final byte[] targetInformation) - throws NTLMEngineException { + Type3Message(final String domain, + final String host, + final String user, + final char[] password, + final byte[] nonce, + final int type2Flags, + final String target, + final byte[] targetInformation, + final Certificate peerServerCertificate, + final byte[] type1Message, + final byte[] type2Message) + throws NTLMEngineException { + this(RND_GEN, System.currentTimeMillis(), domain, host, user, password, nonce, type2Flags, target, targetInformation, peerServerCertificate, type1Message, type2Message); + } + + /** Constructor. Pass the arguments we will need */ + Type3Message(final Random random, final long currentTime, + final String domain, + final String host, + final String user, + final char[] password, + final byte[] nonce, + final int type2Flags, + final String target, + final byte[] targetInformation, + final Certificate peerServerCertificate, + final byte[] type1Message, + final byte[] type2Message) + throws NTLMEngineException { + + if (random == null) { + throw new NTLMEngineException("Random generator not available"); + } + // Save the flags this.type2Flags = type2Flags; + this.type1Message = type1Message; + this.type2Message = type2Message; // All host name manipulations now take place in the credentials final String unqualifiedHost = host; // All domain name manipulations now take place in the credentials final String unqualifiedDomain = domain; - // Create a cipher generator class. Use domain BEFORE it gets modified! - final CipherGen gen = new CipherGen(unqualifiedDomain, user, password, nonce, target, targetInformation); + byte[] responseTargetInformation = targetInformation; + if (peerServerCertificate != null) { + responseTargetInformation = addGssMicAvsToTargetInfo(targetInformation, peerServerCertificate); + computeMic = true; + } else { + computeMic = false; + } + + // Create a cipher generator class. Use domain BEFORE it gets modified! + final CipherGen gen = new CipherGen(random, currentTime, + unqualifiedDomain, + user, + password, + nonce, + target, + responseTargetInformation); // Use the new code to calculate the responses, including v2 if that // seems warranted. @@ -1208,25 +1637,37 @@ final class NTLMEngineImpl implements NTLMEngine { if ((type2Flags & FLAG_REQUEST_SIGN) != 0) { if ((type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0) { - sessionKey = RC4(gen.getSecondaryKey(), userSessionKey); + exportedSessionKey = gen.getSecondaryKey(); + sessionKey = RC4(exportedSessionKey, userSessionKey); } else { sessionKey = userSessionKey; + exportedSessionKey = sessionKey; } } else { + if (computeMic) { + throw new NTLMEngineException("Cannot sign/seal: no exported session key"); + } sessionKey = null; + exportedSessionKey = null; } - if (UNICODE_LITTLE_UNMARKED == null) { - throw new NTLMEngineException("Unicode not supported"); - } - hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(UNICODE_LITTLE_UNMARKED) : null; - domainBytes = unqualifiedDomain != null ? unqualifiedDomain - .toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED) : null; - userBytes = user.getBytes(UNICODE_LITTLE_UNMARKED); + final Charset charset = getCharset(type2Flags); + hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(charset) : null; + domainBytes = unqualifiedDomain != null ? unqualifiedDomain + .toUpperCase(Locale.ROOT).getBytes(charset) : null; + userBytes = user.getBytes(charset); + } + + public byte[] getEncryptedRandomSessionKey() { + return sessionKey; + } + + public byte[] getExportedSessionKey() { + return exportedSessionKey; } /** Assemble the response */ @Override - String getResponse() { + void buildMessage() { final int ntRespLen = ntResp.length; final int lmRespLen = lmResp.length; @@ -1241,7 +1682,8 @@ final class NTLMEngineImpl implements NTLMEngine { } // Calculate the layout within the packet - final int lmRespOffset = 72; // allocate space for the version + final int lmRespOffset = 72 + // allocate space for the version + ( computeMic ? 16 : 0 ); // and MIC final int ntRespOffset = lmRespOffset + lmRespLen; final int domainOffset = ntRespOffset + ntRespLen; final int userOffset = domainOffset + domainLen; @@ -1296,6 +1738,7 @@ final class NTLMEngineImpl implements NTLMEngine { // Flags. addULong( + /* //FLAG_WORKSTATION_PRESENT | //FLAG_DOMAIN_PRESENT | @@ -1320,6 +1763,8 @@ final class NTLMEngineImpl implements NTLMEngine { (type2Flags & FLAG_TARGETINFO_PRESENT) | (type2Flags & FLAG_REQUEST_UNICODE_ENCODING) | (type2Flags & FLAG_REQUEST_TARGET) + */ + type2Flags ); // Version @@ -1329,6 +1774,12 @@ final class NTLMEngineImpl implements NTLMEngine { // NTLM revision addUShort(0x0f00); + int micPosition = -1; + if ( computeMic ) { + micPosition = currentOutputPosition; + currentOutputPosition += 16; + } + // Add the actual data addBytes(lmResp); addBytes(ntResp); @@ -1339,8 +1790,65 @@ final class NTLMEngineImpl implements NTLMEngine { addBytes(sessionKey); } - return super.getResponse(); + // Write the mic back into its slot in the message + + if (computeMic) { + // Computation of message integrity code (MIC) as specified in [MS-NLMP] section 3.2.5.1.2. + final HMACMD5 hmacMD5 = new HMACMD5( exportedSessionKey ); + hmacMD5.update( type1Message ); + hmacMD5.update( type2Message ); + hmacMD5.update( messageContents ); + final byte[] mic = hmacMD5.getOutput(); + System.arraycopy( mic, 0, messageContents, micPosition, mic.length ); + } } + + /** + * Add GSS channel binding hash and MIC flag to the targetInfo. + * Looks like this is needed if we want to use exported session key for GSS wrapping. + */ + private byte[] addGssMicAvsToTargetInfo( final byte[] originalTargetInfo, + final Certificate peerServerCertificate ) throws NTLMEngineException + { + final byte[] newTargetInfo = new byte[originalTargetInfo.length + 8 + 20]; + final int appendLength = originalTargetInfo.length - 4; // last tag is MSV_AV_EOL, do not copy that + System.arraycopy( originalTargetInfo, 0, newTargetInfo, 0, appendLength ); + writeUShort( newTargetInfo, MSV_AV_FLAGS, appendLength ); + writeUShort( newTargetInfo, 4, appendLength + 2 ); + writeULong( newTargetInfo, MSV_AV_FLAGS_MIC, appendLength + 4 ); + writeUShort( newTargetInfo, MSV_AV_CHANNEL_BINDINGS, appendLength + 8 ); + writeUShort( newTargetInfo, 16, appendLength + 10 ); + + byte[] channelBindingsHash; + try + { + final byte[] certBytes = peerServerCertificate.getEncoded(); + final MessageDigest sha256 = MessageDigest.getInstance( "SHA-256" ); + final byte[] certHashBytes = sha256.digest( certBytes ); + final byte[] channelBindingStruct = new byte[16 + 4 + MAGIC_TLS_SERVER_ENDPOINT.length + + certHashBytes.length]; + writeULong( channelBindingStruct, 0x00000035, 16 ); + System.arraycopy( MAGIC_TLS_SERVER_ENDPOINT, 0, channelBindingStruct, 20, + MAGIC_TLS_SERVER_ENDPOINT.length ); + System.arraycopy( certHashBytes, 0, channelBindingStruct, 20 + MAGIC_TLS_SERVER_ENDPOINT.length, + certHashBytes.length ); + final MessageDigest md5 = getMD5(); + channelBindingsHash = md5.digest( channelBindingStruct ); + } + catch ( CertificateEncodingException | NoSuchAlgorithmException e ) + { + throw new NTLMEngineException( e.getMessage(), e ); + } + + System.arraycopy( channelBindingsHash, 0, newTargetInfo, appendLength + 12, 16 ); + return newTargetInfo; + } + + } + + static void writeUShort(final byte[] buffer, final int value, final int offset) { + buffer[offset] = ( byte ) ( value & 0xff ); + buffer[offset + 1] = ( byte ) ( value >> 8 & 0xff ); } static void writeULong(final byte[] buffer, final int value, final int offset) { @@ -1366,6 +1874,14 @@ final class NTLMEngineImpl implements NTLMEngine { return ((val << numbits) | (val >>> (32 - numbits))); } + static MessageDigest getMD5() { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException("MD5 message digest doesn't seem to exist - fatal error: "+ex.getMessage(), ex); + } + } + /** * Cryptography support - MD4. The following class was based loosely on the * RFC and on code found at http://www.cs.umd.edu/~harry/jotp/src/md.java. @@ -1374,12 +1890,12 @@ final class NTLMEngineImpl implements NTLMEngine { * final form found here by Karl Wright (kwright@metacarta.com). */ static class MD4 { - protected int A = 0x67452301; - protected int B = 0xefcdab89; - protected int C = 0x98badcfe; - protected int D = 0x10325476; - protected long count = 0L; - protected final byte[] dataBuffer = new byte[64]; + int A = 0x67452301; + int B = 0xefcdab89; + int C = 0x98badcfe; + int D = 0x10325476; + long count = 0L; + final byte[] dataBuffer = new byte[64]; MD4() { } @@ -1438,7 +1954,7 @@ final class NTLMEngineImpl implements NTLMEngine { return result; } - protected void processBuffer() { + void processBuffer() { // Convert current buffer to 16 ulongs final int[] d = new int[16]; @@ -1463,7 +1979,7 @@ final class NTLMEngineImpl implements NTLMEngine { } - protected void round1(final int[] d) { + void round1(final int[] d) { A = rotintlft((A + F(B, C, D) + d[0]), 3); D = rotintlft((D + F(A, B, C) + d[1]), 7); C = rotintlft((C + F(D, A, B) + d[2]), 11); @@ -1485,7 +2001,7 @@ final class NTLMEngineImpl implements NTLMEngine { B = rotintlft((B + F(C, D, A) + d[15]), 19); } - protected void round2(final int[] d) { + void round2(final int[] d) { A = rotintlft((A + G(B, C, D) + d[0] + 0x5a827999), 3); D = rotintlft((D + G(A, B, C) + d[4] + 0x5a827999), 5); C = rotintlft((C + G(D, A, B) + d[8] + 0x5a827999), 9); @@ -1508,7 +2024,7 @@ final class NTLMEngineImpl implements NTLMEngine { } - protected void round3(final int[] d) { + void round3(final int[] d) { A = rotintlft((A + H(B, C, D) + d[0] + 0x6ed9eba1), 3); D = rotintlft((D + H(A, B, C) + d[8] + 0x6ed9eba1), 9); C = rotintlft((C + H(D, A, B) + d[4] + 0x6ed9eba1), 11); @@ -1538,20 +2054,13 @@ final class NTLMEngineImpl implements NTLMEngine { * resources by Karl Wright */ static class HMACMD5 { - protected final byte[] ipad; - protected final byte[] opad; - protected final MessageDigest md5; + final byte[] ipad; + final byte[] opad; + final MessageDigest md5; - HMACMD5(final byte[] input) throws NTLMEngineException { + HMACMD5(final byte[] input) { byte[] key = input; - try { - md5 = MessageDigest.getInstance("MD5"); - } catch (final Exception ex) { - // Umm, the algorithm doesn't exist - throw an - // NTLMEngineException! - throw new NTLMEngineException( - "Error getting md5 message digest implementation: " + ex.getMessage(), ex); - } + md5 = getMD5(); // Initialize the pad buffers with the key ipad = new byte[64]; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/protocol/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/protocol/DefaultAuthenticationStrategy.java index 7fda71c40..9193b5a53 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/protocol/DefaultAuthenticationStrategy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/protocol/DefaultAuthenticationStrategy.java @@ -68,6 +68,7 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy { AuthSchemes.SPNEGO, AuthSchemes.KERBEROS, AuthSchemes.NTLM, + AuthSchemes.CREDSSP, AuthSchemes.DIGEST, AuthSchemes.BASIC)); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java index ed04c2777..7cfc34314 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java @@ -51,9 +51,11 @@ import org.apache.hc.client5.http.cookie.CookieStore; import org.apache.hc.client5.http.entity.InputStreamFactory; import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy; import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.client5.http.impl.DefaultUserTokenHandler; import org.apache.hc.client5.http.impl.IdleConnectionEvictor; import org.apache.hc.client5.http.impl.NoopUserTokenHandler; import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; +import org.apache.hc.client5.http.impl.auth.CredSspSchemeFactory; import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory; import org.apache.hc.client5.http.impl.auth.KerberosSchemeFactory; import org.apache.hc.client5.http.impl.auth.NTLMSchemeFactory; @@ -62,7 +64,6 @@ import org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.protocol.DefaultAuthenticationStrategy; import org.apache.hc.client5.http.impl.protocol.DefaultRedirectStrategy; -import org.apache.hc.client5.http.impl.DefaultUserTokenHandler; import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; @@ -833,6 +834,7 @@ public class HttpClientBuilder { authSchemeRegistryCopy = RegistryBuilder.create() .register(AuthSchemes.BASIC, new BasicSchemeFactory()) .register(AuthSchemes.DIGEST, new DigestSchemeFactory()) + .register(AuthSchemes.CREDSSP, new CredSspSchemeFactory()) .register(AuthSchemes.NTLM, new NTLMSchemeFactory()) .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(SystemDefaultDnsResolver.INSTANCE, true, true)) .register(AuthSchemes.KERBEROS, new KerberosSchemeFactory(SystemDefaultDnsResolver.INSTANCE, true, true)) diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestNTLMEngineImpl.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestNTLMEngineImpl.java index aba5081f2..2545ce698 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestNTLMEngineImpl.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestNTLMEngineImpl.java @@ -26,7 +26,11 @@ */ package org.apache.hc.client5.http.impl.auth; +import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Random; import org.junit.Assert; import org.junit.Test; @@ -52,6 +56,9 @@ public class TestNTLMEngineImpl { if (c >= 'a' && c <= 'f') { return (byte) (c - 'a' + 0x0a); } + if (c >= 'A' && c <= 'F') { + return (byte) (c - 'A' + 0x0a); + } return (byte) (c - '0'); } @@ -90,6 +97,8 @@ public class TestNTLMEngineImpl { @Test public void testLMResponse() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, null, null, "SecREt01".toCharArray(), @@ -108,6 +117,8 @@ public class TestNTLMEngineImpl { @Test public void testNTLMResponse() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, null, null, "SecREt01".toCharArray(), @@ -126,6 +137,8 @@ public class TestNTLMEngineImpl { @Test public void testLMv2Response() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, "DOMAIN", "user", "SecREt01".toCharArray(), @@ -144,6 +157,8 @@ public class TestNTLMEngineImpl { @Test public void testNTLMv2Response() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, "DOMAIN", "user", "SecREt01".toCharArray(), @@ -164,6 +179,8 @@ public class TestNTLMEngineImpl { @Test public void testLM2SessionResponse() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, "DOMAIN", "user", "SecREt01".toCharArray(), @@ -182,6 +199,8 @@ public class TestNTLMEngineImpl { @Test public void testNTLM2SessionResponse() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, "DOMAIN", "user", "SecREt01".toCharArray(), @@ -200,6 +219,8 @@ public class TestNTLMEngineImpl { @Test public void testNTLMUserSessionKey() throws Exception { final NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + new Random(1234), + 1234L, "DOMAIN", "user", "SecREt01".toCharArray(), @@ -217,20 +238,74 @@ public class TestNTLMEngineImpl { @Test public void testType1Message() throws Exception { - new NTLMEngineImpl().getType1Message("myhost", "mydomain"); + final byte[] bytes = new NTLMEngineImpl.Type1Message("myhost", "mydomain").getBytes(); + final byte[] bytes2 = toBytes("4E544C4D5353500001000000018208A20C000C003800000010001000280000000501280A0000000F6D00790064006F006D00610069006E004D00590048004F0053005400"); + checkArraysMatch(bytes2, bytes); } @Test public void testType3Message() throws Exception { - new NTLMEngineImpl().getType3Message("me", "mypassword".toCharArray(), "myhost", "mydomain", - toBytes("0001020304050607"), - 0xffffffff, - null,null); - new NTLMEngineImpl().getType3Message("me", "mypassword".toCharArray(), "myhost", "mydomain", - toBytes("0001020304050607"), - 0xffffffff, - "mytarget", - toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000")); + final byte[] bytes = new NTLMEngineImpl.Type3Message( + new Random(1234), + 1234L, + "me", "mypassword", "myhost", "mydomain".toCharArray(), + toBytes("0001020304050607"), + 0xffffffff, + null,null).getBytes(); + checkArraysMatch(toBytes("4E544C4D53535000030000001800180048000000180018006000000004000400780000000C000C007C0000001400140088000000100010009C000000FFFFFFFF0501280A0000000FA86886A5D297814200000000000000000000000000000000EEC7568E00798491244959B9C942F4F367C5CBABEEF546F74D0045006D00790068006F00730074006D007900700061007300730077006F007200640094DDAB1EBB82C9A1AB914CAE6F199644"), + bytes); + final byte[] bytes2 = new NTLMEngineImpl.Type3Message( + new Random(1234), + 1234L, + "me", "mypassword", "myhost", "mydomain".toCharArray(), + toBytes("0001020304050607"), + 0xffffffff, + "mytarget", + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000")).getBytes(); + checkArraysMatch(toBytes("4E544C4D53535000030000001800180048000000920092006000000004000400F20000000C000C00F600000014001400020100001000100016010000FFFFFFFF0501280A0000000F3695F1EA7B164788A437892FA7504320DA2D8CF378EBC83CE856A8FB985BF7783545828A91A13AE8010100000000000020CBFAD5DEB19D01A86886A5D29781420000000002000C0044004F004D00410049004E0001000C005300450052005600450052000400140064006F006D00610069006E002E0063006F006D00030022007300650072007600650072002E0064006F006D00610069006E002E0063006F006D0000000000000000004D0045006D00790068006F00730074006D007900700061007300730077006F0072006400BB1AAD36F11631CC7CBC8800CEEE1C99"), + bytes2); + } + + private static final String cannedCert = + "-----BEGIN CERTIFICATE-----\n"+ + "MIIDIDCCAgigAwIBAgIEOqKaWTANBgkqhkiG9w0BAQsFADBSMQswCQYDVQQGEwJVUzEQMA4GA1UEBxMH\n"+ + "TXkgQ2l0eTEYMBYGA1UEChMPTXkgT3JnYW5pemF0aW9uMRcwFQYDVQQDEw5NeSBBcHBsaWNhdGlvbjAe\n"+ + "Fw0xNzAzMTcxNDAyMzRaFw0yNzAzMTUxNDAyMzRaMFIxCzAJBgNVBAYTAlVTMRAwDgYDVQQHEwdNeSBD\n"+ + "aXR5MRgwFgYDVQQKEw9NeSBPcmdhbml6YXRpb24xFzAVBgNVBAMTDk15IEFwcGxpY2F0aW9uMIIBIjAN\n"+ + "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArc+mbViBaHeRSt82KrJ5IF+62b/Qob95Lca4DJIislTY\n"+ + "vLPIo0R1faBV8BkEeUQwo01srkf3RaGLCHNZnFal4KEzbtiUy6W+n08G5E9w9YG+WSwW2dmjvEI7k2a2\n"+ + "xqlaM4NdMKL4ONPXcxfZsMDqxDgpdkaNPKpZ10NDq6rmBTkQw/OSG0z1KLtwLkF1ZQ/3mXdjVzvP83V2\n"+ + "g17AqBazb0Z1YHsVKmkGjPqnq3niJH/6Oke4N+5k/1cE5lSJcQNGP0nqeGdJfvqQZ+gk6gH/sOngZL9X\n"+ + "hPVkpseAwHa+xuPneDSjibLgLmMt3XGDK6jGfjdp5FWqFvAD5E3LHbW9gwIDAQABMA0GCSqGSIb3DQEB\n"+ + "CwUAA4IBAQCpUXUHhl5LyMSO5Q0OktEc9AaFjZtVfknpPde6Zeh35Pqd2354ErvJSBWgzFAphda0oh2s\n"+ + "OIAFkM6LJQEnVDTbXDXN+YY8e3gb9ryfh85hkhC0XI9qp17WPSkmw8XgDfvRd6YQgKm1AnLxjOCwG2jg\n"+ + "i09iZBIWkW3ZeRAMvWPHHjvq44iZB5ZrEl0apgumS6MxpUzKOr5Pcq0jxJDw2UCj5YloFMNl+UINv2vV\n"+ + "aL/DR6ivc61dOfN1E/VNBGkkCk/AogNyucGiFMCq9hd25Y9EbkBBqObYTH1XMX+ufsJh+6hG7KDQ1e/F\n"+ + "nRrlhKwM2uRe+aSH0D6/erjDBT7tXvwn\n"+ + "-----END CERTIFICATE-----"; + + @Test + public void testType3MessageWithCert() throws Exception { + final ByteArrayInputStream fis = new ByteArrayInputStream(cannedCert.getBytes(StandardCharsets.US_ASCII)); + + final CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + final Certificate cert = cf.generateCertificate(fis); + + final byte[] bytes = new NTLMEngineImpl.Type3Message( + new Random(1234), + 1234L, + "me", "mypassword", "myhost", "mydomain".toCharArray(), + toBytes("0001020304050607"), + 0xffffffff, + "mytarget", + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"), + cert, + toBytes("4E544C4D5353500001000000018208A20C000C003800000010001000280000000501280A0000000F6D00790064006F006D00610069006E004D00590048004F0053005400"), + toBytes("4E544C4D5353500001000000018208A20C000C003800000010001000280000000501280A0000000F6D00790064006F006D00610069006E004D00590048004F0053005400FFFEFDFCFBFA")).getBytes(); + + checkArraysMatch(toBytes("4E544C4D53535000030000001800180058000000AE00AE0070000000040004001E0100000C000C0022010000140014002E0100001000100042010000FFFFFFFF0501280A0000000FEEFCCE4187D6CDF1F91C686C4E571D943695F1EA7B164788A437892FA7504320DA2D8CF378EBC83C59D7A3B2951929079B66621D7CF4326B010100000000000020CBFAD5DEB19D01A86886A5D29781420000000002000C0044004F004D00410049004E0001000C005300450052005600450052000400140064006F006D00610069006E002E0063006F006D00030022007300650072007600650072002E0064006F006D00610069006E002E0063006F006D0006000400020000000A00100038EDC0B7EF8D8FE9E1E6A83F6DFEB8FF00000000000000004D0045006D00790068006F00730074006D007900700061007300730077006F0072006400BB1AAD36F11631CC7CBC8800CEEE1C99"), + bytes); } @Test @@ -248,4 +323,5 @@ public class TestNTLMEngineImpl { Assert.assertEquals(a1[i],a2[i]); } } + }