From 44f8924b58fe8860d5ea1be52f0a4363d4f640cc Mon Sep 17 00:00:00 2001 From: Karl Wright Date: Thu, 16 Mar 2017 11:06:46 +0000 Subject: [PATCH] HTTPCLIENT-1834: NTLN auth refactoring; support for MS CredSsp auth Contributed by Radovan Semancik and Karl Wright --- .../impl/auth/win/WindowsNegotiateScheme.java | 3 +- .../http/client/config/AuthSchemes.java | 5 + .../apache/http/impl/auth/CredSspScheme.java | 1126 +++++++++++++++++ .../http/impl/auth/CredSspSchemeFactory.java | 44 + .../org/apache/http/impl/auth/DebugUtil.java | 96 ++ .../apache/http/impl/auth/NTLMEngineImpl.java | 845 ++++++++++--- .../client/AuthenticationStrategyImpl.java | 2 +- .../http/impl/auth/TestNTLMEngineImpl.java | 87 +- 8 files changed, 2006 insertions(+), 202 deletions(-) create mode 100644 httpclient/src/main/java/org/apache/http/impl/auth/CredSspScheme.java create mode 100644 httpclient/src/main/java/org/apache/http/impl/auth/CredSspSchemeFactory.java create mode 100644 httpclient/src/main/java/org/apache/http/impl/auth/DebugUtil.java diff --git a/httpclient-win/src/main/java/org/apache/http/impl/auth/win/WindowsNegotiateScheme.java b/httpclient-win/src/main/java/org/apache/http/impl/auth/win/WindowsNegotiateScheme.java index b07c75ec5..333458bd5 100644 --- a/httpclient-win/src/main/java/org/apache/http/impl/auth/win/WindowsNegotiateScheme.java +++ b/httpclient-win/src/main/java/org/apache/http/impl/auth/win/WindowsNegotiateScheme.java @@ -252,7 +252,8 @@ public class WindowsNegotiateScheme extends AuthSchemeBase { } } else { final HttpClientContext clientContext = HttpClientContext.adapt(context); - final HttpHost target = clientContext.getTargetHost(); if (target != null) { + final HttpHost target = clientContext.getTargetHost(); + if (target != null) { spn = "HTTP/" + target.getHostName(); } else { final RouteInfo route = clientContext.getHttpRoute(); diff --git a/httpclient/src/main/java/org/apache/http/client/config/AuthSchemes.java b/httpclient/src/main/java/org/apache/http/client/config/AuthSchemes.java index da7e26881..f3a0e1baf 100644 --- a/httpclient/src/main/java/org/apache/http/client/config/AuthSchemes.java +++ b/httpclient/src/main/java/org/apache/http/client/config/AuthSchemes.java @@ -65,6 +65,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/httpclient/src/main/java/org/apache/http/impl/auth/CredSspScheme.java b/httpclient/src/main/java/org/apache/http/impl/auth/CredSspScheme.java new file mode 100644 index 000000000..f8db658c9 --- /dev/null +++ b/httpclient/src/main/java/org/apache/http/impl/auth/CredSspScheme.java @@ -0,0 +1,1126 @@ +/* + * ==================================================================== + * 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.http.impl.auth; + + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +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.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.Consts; +import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.auth.AUTH; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.InvalidCredentialsException; +import org.apache.http.auth.MalformedChallengeException; +import org.apache.http.auth.NTCredentials; +import org.apache.http.message.BufferedHeader; +import org.apache.http.protocol.HttpContext; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.util.CharArrayBuffer; +import org.apache.http.util.CharsetUtils; + +/** + *

+ * 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 extends AuthSchemeBase +{ + private static final Charset UNICODE_LITTLE_UNMARKED = CharsetUtils.lookup( "UnicodeLittleUnmarked" ); + public static final String SCHEME_NAME = "CredSSP"; + + private final Log log = LogFactory.getLog( 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 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 getSchemeName() + { + return SCHEME_NAME; + } + + + @Override + public String getParameter( final String name ) + { + return null; + } + + + @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() + { + SSLContext sslContext; + try + { + sslContext = SSLContexts.custom().build(); + } + catch ( NoSuchAlgorithmException e ) + { + throw new RuntimeException( "Error creating SSL Context: " + e.getMessage(), e ); + } + catch ( 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 + protected void parseChallenge( final CharArrayBuffer buffer, final int beginIndex, final int endIndex ) + throws MalformedChallengeException + { + final String inputString = buffer.substringTrimmed( beginIndex, endIndex ); + + 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 + @Deprecated + public Header authenticate( + final Credentials credentials, + final HttpRequest request ) throws AuthenticationException + { + return authenticate( credentials, request, null ); + } + + + @Override + public Header authenticate( + final Credentials credentials, + final HttpRequest request, + final HttpContext context ) throws AuthenticationException + { + NTCredentials ntcredentials = null; + try + { + ntcredentials = ( NTCredentials ) credentials; + } + catch ( final ClassCastException e ) + { + throw new InvalidCredentialsException( + "Credentials cannot be used for CredSSP authentication: " + + credentials.getClass().getName() ); + } + + String outputString = null; + + 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 ); + } + final CharArrayBuffer buffer = new CharArrayBuffer( 32 ); + if ( isProxy() ) + { + buffer.append( AUTH.PROXY_AUTH_RESP ); + } + else + { + buffer.append( AUTH.WWW_AUTH_RESP ); + } + buffer.append( ": CredSSP " ); + buffer.append( outputString ); + return new BufferedHeader( buffer ); + } + + + 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 + { + 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 string.getBytes( UNICODE_LITTLE_UNMARKED ); + } + + + 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), Consts.ASCII); + } + + + private ByteBuffer decodeBase64( final String inputString ) + { + final byte[] inputBytes = Base64.decodeBase64(inputString.getBytes(Consts.ASCII)); + final ByteBuffer buffer = ByteBuffer.wrap( inputBytes ); + return buffer; + } + + + @Override + public boolean isComplete() + { + 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/httpclient/src/main/java/org/apache/http/impl/auth/CredSspSchemeFactory.java b/httpclient/src/main/java/org/apache/http/impl/auth/CredSspSchemeFactory.java new file mode 100644 index 000000000..309101bde --- /dev/null +++ b/httpclient/src/main/java/org/apache/http/impl/auth/CredSspSchemeFactory.java @@ -0,0 +1,44 @@ +/* + * ==================================================================== + * 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.http.impl.auth; + + +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.protocol.HttpContext; + + +public class CredSspSchemeFactory implements AuthSchemeProvider +{ + + @Override + public AuthScheme create( final HttpContext context ) + { + return new CredSspScheme(); + } +} diff --git a/httpclient/src/main/java/org/apache/http/impl/auth/DebugUtil.java b/httpclient/src/main/java/org/apache/http/impl/auth/DebugUtil.java new file mode 100644 index 000000000..862ab357f --- /dev/null +++ b/httpclient/src/main/java/org/apache/http/impl/auth/DebugUtil.java @@ -0,0 +1,96 @@ +/* + * ==================================================================== + * 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.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/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java b/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java index 1a7dc8b57..216f43f45 100644 --- a/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java +++ b/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java @@ -26,20 +26,21 @@ */ package org.apache.http.impl.auth; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; +import org.apache.http.Consts; import java.security.Key; import java.security.MessageDigest; import java.util.Arrays; import java.util.Locale; +import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.Certificate; import org.apache.commons.codec.binary.Base64; -import org.apache.http.Consts; -import org.apache.http.util.CharsetUtils; -import org.apache.http.util.EncodingUtils; /** * Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM @@ -50,7 +51,7 @@ import org.apache.http.util.EncodingUtils; final class NTLMEngineImpl implements NTLMEngine { /** Unicode encoding */ - private static final Charset UNICODE_LITTLE_UNMARKED = CharsetUtils.lookup("UnicodeLittleUnmarked"); + private static final Charset UNICODE_LITTLE_UNMARKED = Charset.forName("UnicodeLittleUnmarked"); /** Character encoding */ private static final Charset DEFAULT_CHARSET = Consts.ASCII; @@ -58,22 +59,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; @@ -87,46 +107,34 @@ 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(Consts.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.2 + 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(Consts.ASCII); + + private static byte[] getNullTerminatedAsciiString( final String source ) + { + final byte[] bytesWithoutNull = source.getBytes(Consts.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(); - /** - * 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. - * @throws org.apache.http.HttpException - * If the messages cannot be retrieved. - */ - static String getResponseFor(final String message, final String username, final String password, - final String host, final String domain) throws NTLMEngineException { - - final String response; - if (message == null || message.trim().equals("")) { - 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()); - } - return response; + NTLMEngineImpl() { } /** @@ -140,7 +148,7 @@ final class NTLMEngineImpl implements NTLMEngine { * The domain to authenticate with. * @return String the message to add to the HTTP request header. */ - static String getType1Message(final String host, final String domain) throws NTLMEngineException { + static String getType1Message(final String host, final String domain) { // For compatibility reason do not include domain and host in type 1 message //return new Type1Message(domain, host).getResponse(); return TYPE_1_MESSAGE; @@ -173,26 +181,32 @@ final class NTLMEngineImpl implements NTLMEngine { targetInformation).getResponse(); } - /** 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); + /** + * 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. + * @throws NTLMEngineException + * If {@encrypt(byte[],byte[])} fails. + */ + static String getType3Message(final String user, final String 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 { @@ -223,31 +237,28 @@ 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 { + protected final Random random; + protected final long currentTime; + protected final String domain; protected final String user; protected final String password; @@ -279,10 +290,25 @@ final class NTLMEngineImpl implements NTLMEngine { protected byte[] ntlm2SessionResponseUserSessionKey = null; protected byte[] lanManagerSessionKey = null; + @Deprecated public CipherGen(final String domain, final String user, final String password, final byte[] challenge, final String target, final byte[] targetInformation, final byte[] clientChallenge, final byte[] clientChallenge2, final byte[] secondaryKey, final byte[] timestamp) { + this(RND_GEN, System.currentTimeMillis(), + domain, user, password, challenge, target, targetInformation, + clientChallenge, clientChallenge2, + secondaryKey, timestamp); + } + + public CipherGen(final Random random, final long currentTime, + final String domain, final String user, final String 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; @@ -295,16 +321,31 @@ final class NTLMEngineImpl implements NTLMEngine { this.timestamp = timestamp; } - public CipherGen(final String domain, final String user, final String password, - final byte[] challenge, final String target, final byte[] targetInformation) { - this(domain, user, password, challenge, target, targetInformation, null, null, null, null); + @Deprecated + public CipherGen(final String domain, + final String user, + final String password, + final byte[] challenge, + final String target, + final byte[] targetInformation) { + this(RND_GEN, System.currentTimeMillis(), domain, user, password, challenge, target, targetInformation); + } + + public CipherGen(final Random random, final long currentTime, + final String domain, + final String user, + final String 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; } @@ -313,7 +354,7 @@ final class NTLMEngineImpl implements NTLMEngine { public byte[] getClientChallenge2() throws NTLMEngineException { if (clientChallenge2 == null) { - clientChallenge2 = makeRandomChallenge(); + clientChallenge2 = makeRandomChallenge(random); } return clientChallenge2; } @@ -322,7 +363,7 @@ final class NTLMEngineImpl implements NTLMEngine { public byte[] getSecondaryKey() throws NTLMEngineException { if (secondaryKey == null) { - secondaryKey = makeSecondaryKey(); + secondaryKey = makeSecondaryKey(random); } return secondaryKey; } @@ -384,7 +425,7 @@ final class NTLMEngineImpl implements NTLMEngine { /** Calculate a timestamp */ public byte[] getTimestamp() { if (timestamp == null) { - long time = System.currentTimeMillis(); + long time = this.currentTime; time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch. time *= 10000; // tenths of a microsecond. // convert to little-endian byte array. @@ -552,7 +593,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(); @@ -719,6 +760,191 @@ final class NTLMEngineImpl implements NTLMEngine { return lmv2Response; } + static enum Mode + { + CLIENT, SERVER; + } + + static class Handle + { + final private byte[] exportedSessionKey; + private byte[] signingKey; + private byte[] sealingKey; + private 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.exportedSessionKey = exportedSessionKey; + 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 + { + 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. @@ -802,21 +1028,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; + protected byte[] messageContents = null; /** The current output position */ - private int currentOutputPosition = 0; + protected 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"); @@ -888,7 +1158,7 @@ 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). */ @@ -946,18 +1216,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 EncodingUtils.getAsciiString(Base64.encodeBase64(resp)); + public String getResponse() { + return new String(Base64.encodeBase64(getBytes()), Consts.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; + } + + protected void buildMessage() { + throw new RuntimeException("Message builder not implemented for "+getClass().getName()); + } } /** Type 1 message assembly class */ @@ -965,12 +1243,16 @@ 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"); - } + this.flags = ((flags == null)?getDefaultFlags():flags); + // Strip off domain name from the host! final String unqualifiedHost = convertHost(host); // Use only the base domain name! @@ -986,56 +1268,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() { + protected 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); @@ -1055,20 +1353,22 @@ final class NTLMEngineImpl implements NTLMEngine { if (domainBytes != null) { addBytes(domainBytes); } - - return super.getResponse(); } } /** Type 2 message class */ static class Type2Message extends NTLMMessage { - protected byte[] challenge; + protected final byte[] challenge; protected String target; protected byte[] targetInfo; - protected int flags; + protected 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: @@ -1091,12 +1391,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 @@ -1105,11 +1399,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)); } } @@ -1148,32 +1438,113 @@ final class NTLMEngineImpl implements NTLMEngine { /** Type 3 message assembly class */ static class Type3Message extends NTLMMessage { + // For mic computation + protected final byte[] type1Message; + protected final byte[] type2Message; // Response flags from the type2 message - protected int type2Flags; + protected final int type2Flags; - protected byte[] domainBytes; - protected byte[] hostBytes; - protected byte[] userBytes; + protected final byte[] domainBytes; + protected final byte[] hostBytes; + protected final byte[] userBytes; protected byte[] lmResp; protected byte[] ntResp; - protected byte[] sessionKey; + protected final byte[] sessionKey; + protected final byte[] exportedSessionKey; + protected final boolean computeMic; + + /** More primitive constructor: don't include cert or previous messages. + */ + Type3Message(final String domain, + final String host, + final String user, + final String 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 String 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 String 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 String 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 String 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; // Strip off domain name from the host! final String unqualifiedHost = convertHost(host); // Use only the base domain name! final String unqualifiedDomain = convertDomain(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. @@ -1226,25 +1597,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() { + protected void buildMessage() { final int ntRespLen = ntResp.length; final int lmRespLen = lmResp.length; @@ -1259,7 +1642,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; @@ -1314,6 +1698,7 @@ final class NTLMEngineImpl implements NTLMEngine { // Flags. addULong( + /* //FLAG_WORKSTATION_PRESENT | //FLAG_DOMAIN_PRESENT | @@ -1338,6 +1723,8 @@ final class NTLMEngineImpl implements NTLMEngine { (type2Flags & FLAG_TARGETINFO_PRESENT) | (type2Flags & FLAG_REQUEST_UNICODE_ENCODING) | (type2Flags & FLAG_REQUEST_TARGET) + */ + type2Flags ); // Version @@ -1347,6 +1734,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); @@ -1357,8 +1750,69 @@ 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 e ) + { + throw new NTLMEngineException( e.getMessage(), e ); + } + catch ( 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) { @@ -1384,6 +1838,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. @@ -1397,7 +1859,7 @@ final class NTLMEngineImpl implements NTLMEngine { protected int C = 0x98badcfe; protected int D = 0x10325476; protected long count = 0L; - protected byte[] dataBuffer = new byte[64]; + protected final byte[] dataBuffer = new byte[64]; MD4() { } @@ -1556,20 +2018,13 @@ final class NTLMEngineImpl implements NTLMEngine { * resources by Karl Wright */ static class HMACMD5 { - protected byte[] ipad; - protected byte[] opad; - protected MessageDigest md5; + protected final byte[] ipad; + protected final byte[] opad; + protected 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]; @@ -1594,7 +2049,7 @@ final class NTLMEngineImpl implements NTLMEngine { i++; } - // Very important: update the digest with the ipad buffer + // Very important: processChallenge the digest with the ipad buffer md5.reset(); md5.update(ipad); diff --git a/httpclient/src/main/java/org/apache/http/impl/client/AuthenticationStrategyImpl.java b/httpclient/src/main/java/org/apache/http/impl/client/AuthenticationStrategyImpl.java index 57df6141f..1f35e61c5 100644 --- a/httpclient/src/main/java/org/apache/http/impl/client/AuthenticationStrategyImpl.java +++ b/httpclient/src/main/java/org/apache/http/impl/client/AuthenticationStrategyImpl.java @@ -73,9 +73,9 @@ abstract class AuthenticationStrategyImpl implements AuthenticationStrategy { AuthSchemes.SPNEGO, AuthSchemes.KERBEROS, AuthSchemes.NTLM, + AuthSchemes.CREDSSP, AuthSchemes.DIGEST, AuthSchemes.BASIC)); - private final int challengeCode; private final String headerName; diff --git a/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java b/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java index 03900e24f..8af33b622 100644 --- a/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java +++ b/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java @@ -29,6 +29,11 @@ package org.apache.http.impl.auth; import org.apache.http.Consts; import org.junit.Assert; import org.junit.Test; +import java.util.Random; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.Certificate; public class TestNTLMEngineImpl { @@ -51,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'); } @@ -89,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", @@ -107,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", @@ -125,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", @@ -143,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", @@ -163,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", @@ -181,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", @@ -199,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", @@ -216,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", "myhost", "mydomain", + final byte[] bytes = new NTLMEngineImpl.Type3Message( + new Random(1234), + 1234L, + "me", "mypassword", "myhost", "mydomain", toBytes("0001020304050607"), 0xffffffff, - null,null); - new NTLMEngineImpl().getType3Message("me", "mypassword", "myhost", "mydomain", + null,null).getBytes(); + checkArraysMatch(toBytes("4E544C4D53535000030000001800180048000000180018006000000004000400780000000C000C007C0000001400140088000000100010009C000000FFFFFFFF0501280A0000000FA86886A5D297814200000000000000000000000000000000EEC7568E00798491244959B9C942F4F367C5CBABEEF546F74D0045006D00790068006F00730074006D007900700061007300730077006F007200640094DDAB1EBB82C9A1AB914CAE6F199644"), + bytes); + final byte[] bytes2 = new NTLMEngineImpl.Type3Message( + new Random(1234), + 1234L, + "me", "mypassword", "myhost", "mydomain", toBytes("0001020304050607"), 0xffffffff, "mytarget", - toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000")); + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000")).getBytes(); + checkArraysMatch(toBytes("4E544C4D53535000030000001800180048000000920092006000000004000400F20000000C000C00F600000014001400020100001000100016010000FFFFFFFF0501280A0000000F3695F1EA7B164788A437892FA7504320DA2D8CF378EBC83CE856A8FB985BF7783545828A91A13AE8010100000000000020CBFAD5DEB19D01A86886A5D29781420000000002000C0044004F004D00410049004E0001000C005300450052005600450052000400140064006F006D00610069006E002E0063006F006D00030022007300650072007600650072002E0064006F006D00610069006E002E0063006F006D0000000000000000004D0045006D00790068006F00730074006D007900700061007300730077006F0072006400BB1AAD36F11631CC7CBC8800CEEE1C99"), + bytes2); + } + + private static 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(Consts.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", + toBytes("0001020304050607"), + 0xffffffff, + "mytarget", + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"), + cert, + toBytes("4E544C4D5353500001000000018208A20C000C003800000010001000280000000501280A0000000F6D00790064006F006D00610069006E004D00590048004F0053005400"), + toBytes("4E544C4D5353500001000000018208A20C000C003800000010001000280000000501280A0000000F6D00790064006F006D00610069006E004D00590048004F0053005400FFFEFDFCFBFA")).getBytes(); + + checkArraysMatch(toBytes("4E544C4D53535000030000001800180058000000AE00AE0070000000040004001E0100000C000C0022010000140014002E0100001000100042010000FFFFFFFF0501280A0000000FEEFCCE4187D6CDF1F91C686C4E571D943695F1EA7B164788A437892FA7504320DA2D8CF378EBC83C59D7A3B2951929079B66621D7CF4326B010100000000000020CBFAD5DEB19D01A86886A5D29781420000000002000C0044004F004D00410049004E0001000C005300450052005600450052000400140064006F006D00610069006E002E0063006F006D00030022007300650072007600650072002E0064006F006D00610069006E002E0063006F006D0006000400020000000A00100038EDC0B7EF8D8FE9E1E6A83F6DFEB8FF00000000000000004D0045006D00790068006F00730074006D007900700061007300730077006F0072006400BB1AAD36F11631CC7CBC8800CEEE1C99"), + bytes); } @Test @@ -247,4 +323,5 @@ public class TestNTLMEngineImpl { Assert.assertEquals(a1[i],a2[i]); } } + }