From 944ced70e1e2f889eafe6b169a9c69396ae8cbee Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Tue, 1 Nov 2022 21:23:33 +0700 Subject: Span-based TlsStream methods --- crypto/src/tls/RecordStream.cs | 51 ++++++++++ crypto/src/tls/TlsProtocol.cs | 141 +++++++++++++++++++++++++++ crypto/src/tls/TlsStream.cs | 14 +++ crypto/src/tls/crypto/TlsCipher.cs | 5 + crypto/src/tls/crypto/TlsNullNullCipher.cs | 10 ++ crypto/src/tls/crypto/impl/TlsAeadCipher.cs | 80 +++++++++++++++ crypto/src/tls/crypto/impl/TlsBlockCipher.cs | 76 +++++++++++++++ crypto/src/tls/crypto/impl/TlsNullCipher.cs | 12 +++ crypto/src/tls/crypto/impl/TlsSuiteHmac.cs | 26 +++++ crypto/src/tls/crypto/impl/TlsSuiteMac.cs | 4 + 10 files changed, 419 insertions(+) (limited to 'crypto/src') diff --git a/crypto/src/tls/RecordStream.cs b/crypto/src/tls/RecordStream.cs index a97d34698..a5926d05b 100644 --- a/crypto/src/tls/RecordStream.cs +++ b/crypto/src/tls/RecordStream.cs @@ -258,6 +258,9 @@ namespace Org.BouncyCastle.Tls /// internal void WriteRecord(short contentType, byte[] plaintext, int plaintextOffset, int plaintextLength) { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + WriteRecord(contentType, plaintext.AsSpan(plaintextOffset, plaintextLength)); +#else // Never send anything until a valid ClientHello has been received if (m_writeVersion == null) return; @@ -298,8 +301,56 @@ namespace Org.BouncyCastle.Tls //} m_output.Flush(); +#endif } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + /// + internal void WriteRecord(short contentType, ReadOnlySpan plaintext) + { + // Never send anything until a valid ClientHello has been received + if (m_writeVersion == null) + return; + + /* + * RFC 5246 6.2.1 The length should not exceed 2^14. + */ + CheckLength(plaintext.Length, m_plaintextLimit, AlertDescription.internal_error); + + /* + * RFC 5246 6.2.1 Implementations MUST NOT send zero-length fragments of Handshake, Alert, + * or ChangeCipherSpec content types. + */ + if (plaintext.Length < 1 && contentType != ContentType.application_data) + throw new TlsFatalAlert(AlertDescription.internal_error); + + long seqNo = m_writeSeqNo.NextValue(AlertDescription.internal_error); + ProtocolVersion recordVersion = m_writeVersion; + + TlsEncodeResult encoded = m_writeCipher.EncodePlaintext(seqNo, contentType, recordVersion, + RecordFormat.FragmentOffset, plaintext); + + int ciphertextLength = encoded.len - RecordFormat.FragmentOffset; + TlsUtilities.CheckUint16(ciphertextLength); + + TlsUtilities.WriteUint8(encoded.recordType, encoded.buf, encoded.off + RecordFormat.TypeOffset); + TlsUtilities.WriteVersion(recordVersion, encoded.buf, encoded.off + RecordFormat.VersionOffset); + TlsUtilities.WriteUint16(ciphertextLength, encoded.buf, encoded.off + RecordFormat.LengthOffset); + + // TODO[tls-port] Can we support interrupted IO on .NET? + //try + //{ + m_output.Write(encoded.buf, encoded.off, encoded.len); + //} + //catch (InterruptedIOException e) + //{ + // throw new TlsFatalAlert(AlertDescription.internal_error, e); + //} + + m_output.Flush(); + } +#endif + /// internal void Close() { diff --git a/crypto/src/tls/TlsProtocol.cs b/crypto/src/tls/TlsProtocol.cs index 3461e9b58..437a51447 100644 --- a/crypto/src/tls/TlsProtocol.cs +++ b/crypto/src/tls/TlsProtocol.cs @@ -707,6 +707,9 @@ namespace Org.BouncyCastle.Tls { Streams.ValidateBufferArguments(buffer, offset, count); +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return ReadApplicationData(buffer.AsSpan(offset, count)); +#else if (!m_appDataReady) throw new InvalidOperationException("Cannot read application data until initial handshake completed."); @@ -733,8 +736,42 @@ namespace Org.BouncyCastle.Tls m_applicationDataQueue.RemoveData(buffer, offset, count, 0); } return count; +#endif } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual int ReadApplicationData(Span buffer) + { + if (!m_appDataReady) + throw new InvalidOperationException("Cannot read application data until initial handshake completed."); + + while (m_applicationDataQueue.Available < 1) + { + if (this.m_closed) + { + if (this.m_failedWithError) + throw new IOException("Cannot read application data on failed TLS connection"); + + return 0; + } + + /* + * NOTE: Only called more than once when empty records are received, so no special + * InterruptedIOException handling is necessary. + */ + SafeReadRecord(); + } + + int count = buffer.Length; + if (count > 0) + { + count = System.Math.Min(count, m_applicationDataQueue.Available); + m_applicationDataQueue.RemoveData(buffer[..count], 0); + } + return count; + } +#endif + /// protected virtual RecordPreview SafePreviewRecordHeader(byte[] recordHeader) { @@ -850,6 +887,32 @@ namespace Org.BouncyCastle.Tls } } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + /// + protected virtual void SafeWriteRecord(short type, ReadOnlySpan buffer) + { + try + { + m_recordStream.WriteRecord(type, buffer); + } + catch (TlsFatalAlert e) + { + HandleException(e.AlertDescription, "Failed to write record", e); + throw e; + } + catch (IOException e) + { + HandleException(AlertDescription.internal_error, "Failed to write record", e); + throw e; + } + catch (Exception e) + { + HandleException(AlertDescription.internal_error, "Failed to write record", e); + throw new TlsFatalAlert(AlertDescription.internal_error, e); + } + } +#endif + /// Write some application data. /// /// Fragmentation is handled internally. Usable in both blocking/non-blocking modes.

@@ -869,6 +932,9 @@ namespace Org.BouncyCastle.Tls { Streams.ValidateBufferArguments(buffer, offset, count); +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + WriteApplicationData(buffer.AsSpan(offset, count)); +#else if (!m_appDataReady) throw new InvalidOperationException( "Cannot write application data until initial handshake completed."); @@ -938,7 +1004,82 @@ namespace Org.BouncyCastle.Tls count -= toWrite; } } +#endif + } + + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual void WriteApplicationData(ReadOnlySpan buffer) + { + if (!m_appDataReady) + throw new InvalidOperationException( + "Cannot write application data until initial handshake completed."); + + lock (m_recordWriteLock) + { + while (!buffer.IsEmpty) + { + if (m_closed) + throw new IOException("Cannot write application data on closed/failed TLS connection"); + + /* + * RFC 5246 6.2.1. Zero-length fragments of Application data MAY be sent as they are + * potentially useful as a traffic analysis countermeasure. + * + * NOTE: Actually, implementations appear to have settled on 1/n-1 record splitting. + */ + if (m_appDataSplitEnabled) + { + /* + * Protect against known IV attack! + * + * DO NOT REMOVE THIS CODE, EXCEPT YOU KNOW EXACTLY WHAT YOU ARE DOING HERE. + */ + switch (m_appDataSplitMode) + { + case ADS_MODE_0_N_FIRSTONLY: + { + this.m_appDataSplitEnabled = false; + SafeWriteRecord(ContentType.application_data, TlsUtilities.EmptyBytes, 0, 0); + break; + } + case ADS_MODE_0_N: + { + SafeWriteRecord(ContentType.application_data, TlsUtilities.EmptyBytes, 0, 0); + break; + } + case ADS_MODE_1_Nsub1: + default: + { + if (buffer.Length > 1) + { + SafeWriteRecord(ContentType.application_data, buffer[..1]); + buffer = buffer[1..]; + } + break; + } + } + } + else if (m_keyUpdateEnabled) + { + if (m_keyUpdatePendingSend) + { + Send13KeyUpdate(false); + } + else if (m_recordStream.NeedsKeyUpdate()) + { + Send13KeyUpdate(true); + } + } + + // Fragment data according to the current fragment limit. + int toWrite = System.Math.Min(buffer.Length, m_recordStream.PlaintextLimit); + SafeWriteRecord(ContentType.application_data, buffer[..toWrite]); + buffer = buffer[toWrite..]; + } + } } +#endif public virtual int AppDataSplitMode { diff --git a/crypto/src/tls/TlsStream.cs b/crypto/src/tls/TlsStream.cs index 01b990799..5c07da2bf 100644 --- a/crypto/src/tls/TlsStream.cs +++ b/crypto/src/tls/TlsStream.cs @@ -58,6 +58,13 @@ namespace Org.BouncyCastle.Tls return m_handler.ReadApplicationData(buffer, offset, count); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public override int Read(Span buffer) + { + return m_handler.ReadApplicationData(buffer); + } +#endif + public override int ReadByte() { byte[] buf = new byte[1]; @@ -80,6 +87,13 @@ namespace Org.BouncyCastle.Tls m_handler.WriteApplicationData(buffer, offset, count); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public override void Write(ReadOnlySpan buffer) + { + m_handler.WriteApplicationData(buffer); + } +#endif + public override void WriteByte(byte value) { m_handler.WriteApplicationData(new byte[]{ value }, 0, 1); diff --git a/crypto/src/tls/crypto/TlsCipher.cs b/crypto/src/tls/crypto/TlsCipher.cs index 4c2147bf7..53a8141fd 100644 --- a/crypto/src/tls/crypto/TlsCipher.cs +++ b/crypto/src/tls/crypto/TlsCipher.cs @@ -38,6 +38,11 @@ namespace Org.BouncyCastle.Tls.Crypto TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, int headerAllocation, byte[] plaintext, int offset, int len); +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, + int headerAllocation, ReadOnlySpan plaintext); +#endif + /// Decode the passed in ciphertext using the current bulk cipher. /// sequence number of the message represented by ciphertext. /// content type used in the record for this message. diff --git a/crypto/src/tls/crypto/TlsNullNullCipher.cs b/crypto/src/tls/crypto/TlsNullNullCipher.cs index 082dff358..13fe092f7 100644 --- a/crypto/src/tls/crypto/TlsNullNullCipher.cs +++ b/crypto/src/tls/crypto/TlsNullNullCipher.cs @@ -31,6 +31,16 @@ namespace Org.BouncyCastle.Tls.Crypto return new TlsEncodeResult(result, 0, result.Length, contentType); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, + int headerAllocation, ReadOnlySpan plaintext) + { + byte[] result = new byte[headerAllocation + plaintext.Length]; + plaintext.CopyTo(result.AsSpan(headerAllocation)); + return new TlsEncodeResult(result, 0, result.Length, contentType); + } +#endif + public TlsDecodeResult DecodeCiphertext(long seqNo, short recordType, ProtocolVersion recordVersion, byte[] ciphertext, int offset, int len) { diff --git a/crypto/src/tls/crypto/impl/TlsAeadCipher.cs b/crypto/src/tls/crypto/impl/TlsAeadCipher.cs index 046e6883f..a53e1e835 100644 --- a/crypto/src/tls/crypto/impl/TlsAeadCipher.cs +++ b/crypto/src/tls/crypto/impl/TlsAeadCipher.cs @@ -161,6 +161,10 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl public virtual TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, int headerAllocation, byte[] plaintext, int plaintextOffset, int plaintextLength) { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return EncodePlaintext(seqNo, contentType, recordVersion, headerAllocation, + plaintext.AsSpan(plaintextOffset, plaintextLength)); +#else byte[] nonce = new byte[m_encryptNonce.Length + m_record_iv_length]; switch (m_nonceMode) @@ -228,8 +232,84 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl throw new TlsFatalAlert(AlertDescription.internal_error); } + return new TlsEncodeResult(output, 0, output.Length, recordType); +#endif + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, + int headerAllocation, ReadOnlySpan plaintext) + { + byte[] nonce = new byte[m_encryptNonce.Length + m_record_iv_length]; + + switch (m_nonceMode) + { + case NONCE_RFC5288: + Array.Copy(m_encryptNonce, 0, nonce, 0, m_encryptNonce.Length); + // RFC 5288/6655: The nonce_explicit MAY be the 64-bit sequence number. + TlsUtilities.WriteUint64(seqNo, nonce, m_encryptNonce.Length); + break; + case NONCE_RFC7905: + TlsUtilities.WriteUint64(seqNo, nonce, nonce.Length - 8); + for (int i = 0; i < m_encryptNonce.Length; ++i) + { + nonce[i] ^= m_encryptNonce[i]; + } + break; + default: + throw new TlsFatalAlert(AlertDescription.internal_error); + } + + int extraLength = m_isTlsV13 ? 1 : 0; + + // TODO[tls13] If we support adding padding to TLSInnerPlaintext, this will need review + int encryptionLength = m_encryptCipher.GetOutputSize(plaintext.Length + extraLength); + int ciphertextLength = m_record_iv_length + encryptionLength; + + byte[] output = new byte[headerAllocation + ciphertextLength]; + int outputPos = headerAllocation; + + if (m_record_iv_length != 0) + { + Array.Copy(nonce, nonce.Length - m_record_iv_length, output, outputPos, m_record_iv_length); + outputPos += m_record_iv_length; + } + + short recordType = m_isTlsV13 ? ContentType.application_data : contentType; + + byte[] additionalData = GetAdditionalData(seqNo, recordType, recordVersion, ciphertextLength, + plaintext.Length); + + try + { + plaintext.CopyTo(output.AsSpan(outputPos)); + if (m_isTlsV13) + { + output[outputPos + plaintext.Length] = (byte)contentType; + } + + m_encryptCipher.Init(nonce, m_macSize, additionalData); + outputPos += m_encryptCipher.DoFinal(output, outputPos, plaintext.Length + extraLength, output, + outputPos); + } + catch (IOException e) + { + throw e; + } + catch (Exception e) + { + throw new TlsFatalAlert(AlertDescription.internal_error, e); + } + + if (outputPos != output.Length) + { + // NOTE: The additional data mechanism for AEAD ciphers requires exact output size prediction. + throw new TlsFatalAlert(AlertDescription.internal_error); + } + return new TlsEncodeResult(output, 0, output.Length, recordType); } +#endif public virtual TlsDecodeResult DecodeCiphertext(long seqNo, short recordType, ProtocolVersion recordVersion, byte[] ciphertext, int ciphertextOffset, int ciphertextLength) diff --git a/crypto/src/tls/crypto/impl/TlsBlockCipher.cs b/crypto/src/tls/crypto/impl/TlsBlockCipher.cs index ed9d68649..64a73bfea 100644 --- a/crypto/src/tls/crypto/impl/TlsBlockCipher.cs +++ b/crypto/src/tls/crypto/impl/TlsBlockCipher.cs @@ -199,6 +199,9 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl public virtual TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, int headerAllocation, byte[] plaintext, int offset, int len) { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return EncodePlaintext(seqNo, contentType, recordVersion, headerAllocation, plaintext.AsSpan(offset, len)); +#else int blockSize = m_encryptCipher.GetBlockSize(); int macSize = m_writeMac.Size; @@ -260,11 +263,84 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl outOff += mac.Length; } + if (outOff != outBuf.Length) + throw new TlsFatalAlert(AlertDescription.internal_error); + + return new TlsEncodeResult(outBuf, 0, outBuf.Length, contentType); +#endif + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, + int headerAllocation, ReadOnlySpan plaintext) + { + int blockSize = m_encryptCipher.GetBlockSize(); + int macSize = m_writeMac.Size; + + int enc_input_length = plaintext.Length; + if (!m_encryptThenMac) + { + enc_input_length += macSize; + } + + int padding_length = blockSize - (enc_input_length % blockSize); + if (m_useExtraPadding) + { + // Add a random number of extra blocks worth of padding + int maxExtraPadBlocks = (256 - padding_length) / blockSize; + int actualExtraPadBlocks = ChooseExtraPadBlocks(maxExtraPadBlocks); + padding_length += actualExtraPadBlocks * blockSize; + } + + int totalSize = plaintext.Length + macSize + padding_length; + if (m_useExplicitIV) + { + totalSize += blockSize; + } + + byte[] outBuf = new byte[headerAllocation + totalSize]; + int outOff = headerAllocation; + + if (m_useExplicitIV) + { + // Technically the explicit IV will be the encryption of this nonce + byte[] explicitIV = m_cryptoParams.NonceGenerator.GenerateNonce(blockSize); + Array.Copy(explicitIV, 0, outBuf, outOff, blockSize); + outOff += blockSize; + } + + plaintext.CopyTo(outBuf.AsSpan(outOff)); + outOff += plaintext.Length; + + if (!m_encryptThenMac) + { + byte[] mac = m_writeMac.CalculateMac(seqNo, contentType, plaintext); + mac.CopyTo(outBuf.AsSpan(outOff)); + outOff += mac.Length; + } + + byte padByte = (byte)(padding_length - 1); + for (int i = 0; i < padding_length; ++i) + { + outBuf[outOff++] = padByte; + } + + m_encryptCipher.DoFinal(outBuf, headerAllocation, outOff - headerAllocation, outBuf, headerAllocation); + + if (m_encryptThenMac) + { + byte[] mac = m_writeMac.CalculateMac(seqNo, contentType, outBuf, headerAllocation, + outOff - headerAllocation); + Array.Copy(mac, 0, outBuf, outOff, mac.Length); + outOff += mac.Length; + } + if (outOff != outBuf.Length) throw new TlsFatalAlert(AlertDescription.internal_error); return new TlsEncodeResult(outBuf, 0, outBuf.Length, contentType); } +#endif public virtual TlsDecodeResult DecodeCiphertext(long seqNo, short recordType, ProtocolVersion recordVersion, byte[] ciphertext, int offset, int len) diff --git a/crypto/src/tls/crypto/impl/TlsNullCipher.cs b/crypto/src/tls/crypto/impl/TlsNullCipher.cs index 5b6b5663a..9bb08110a 100644 --- a/crypto/src/tls/crypto/impl/TlsNullCipher.cs +++ b/crypto/src/tls/crypto/impl/TlsNullCipher.cs @@ -81,6 +81,18 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl return new TlsEncodeResult(ciphertext, 0, ciphertext.Length, contentType); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual TlsEncodeResult EncodePlaintext(long seqNo, short contentType, ProtocolVersion recordVersion, + int headerAllocation, ReadOnlySpan plaintext) + { + byte[] mac = m_writeMac.CalculateMac(seqNo, contentType, plaintext); + byte[] ciphertext = new byte[headerAllocation + plaintext.Length + mac.Length]; + plaintext.CopyTo(ciphertext.AsSpan(headerAllocation)); + mac.CopyTo(ciphertext.AsSpan(headerAllocation + plaintext.Length)); + return new TlsEncodeResult(ciphertext, 0, ciphertext.Length, contentType); + } +#endif + public virtual TlsDecodeResult DecodeCiphertext(long seqNo, short recordType, ProtocolVersion recordVersion, byte[] ciphertext, int offset, int len) { diff --git a/crypto/src/tls/crypto/impl/TlsSuiteHmac.cs b/crypto/src/tls/crypto/impl/TlsSuiteHmac.cs index 9f43f4382..b4edde760 100644 --- a/crypto/src/tls/crypto/impl/TlsSuiteHmac.cs +++ b/crypto/src/tls/crypto/impl/TlsSuiteHmac.cs @@ -55,6 +55,9 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl public virtual byte[] CalculateMac(long seqNo, short type, byte[] msg, int msgOff, int msgLen) { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return CalculateMac(seqNo, type, msg.AsSpan(msgOff, msgLen)); +#else ProtocolVersion serverVersion = m_cryptoParams.ServerVersion; bool isSsl = serverVersion.IsSsl; @@ -71,8 +74,31 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl m_mac.Update(msg, msgOff, msgLen); return Truncate(m_mac.CalculateMac()); +#endif } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual byte[] CalculateMac(long seqNo, short type, ReadOnlySpan message) + { + ProtocolVersion serverVersion = m_cryptoParams.ServerVersion; + bool isSsl = serverVersion.IsSsl; + + byte[] macHeader = new byte[isSsl ? 11 : 13]; + TlsUtilities.WriteUint64(seqNo, macHeader, 0); + TlsUtilities.WriteUint8(type, macHeader, 8); + if (!isSsl) + { + TlsUtilities.WriteVersion(serverVersion, macHeader, 9); + } + TlsUtilities.WriteUint16(message.Length, macHeader, macHeader.Length - 2); + + m_mac.Update(macHeader); + m_mac.Update(message); + + return Truncate(m_mac.CalculateMac()); + } +#endif + public virtual byte[] CalculateMacConstantTime(long seqNo, short type, byte[] msg, int msgOff, int msgLen, int fullLength, byte[] dummyData) { diff --git a/crypto/src/tls/crypto/impl/TlsSuiteMac.cs b/crypto/src/tls/crypto/impl/TlsSuiteMac.cs index 6e4942928..1a28eba81 100644 --- a/crypto/src/tls/crypto/impl/TlsSuiteMac.cs +++ b/crypto/src/tls/crypto/impl/TlsSuiteMac.cs @@ -18,6 +18,10 @@ namespace Org.BouncyCastle.Tls.Crypto.Impl /// A new byte array containing the MAC value. byte[] CalculateMac(long seqNo, short type, byte[] message, int offset, int length); +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + byte[] CalculateMac(long seqNo, short type, ReadOnlySpan message); +#endif + /// Constant time calculation of the MAC for some given data with a given expected length. /// The sequence number of the record. /// The content type of the message. -- cgit 1.4.1