/// <summary> /// Expand a secret key /// </summary> /// <param name="output">Output span. Length determines how much data to generate</param> /// <param name="key">Original key to expand</param> /// <param name="label">Label (treated as a salt)</param> /// <param name="initialSeed">Seed for expansion (treated as a salt)</param> public static void ExpandSecret(ByteSpan output, ByteSpan key, ByteSpan label, ByteSpan initialSeed) { ByteSpan writer = output; byte[] roundSeed = new byte[label.Length + initialSeed.Length]; label.CopyTo(roundSeed); initialSeed.CopyTo(roundSeed, label.Length); byte[] hashA = roundSeed; using (HMACSHA256 hmac = new HMACSHA256(key.ToArray())) { byte[] input = new byte[hmac.OutputBlockSize + roundSeed.Length]; new ByteSpan(roundSeed).CopyTo(input, hmac.OutputBlockSize); while (writer.Length > 0) { // Update hashA hashA = hmac.ComputeHash(hashA); // generate hash input new ByteSpan(hashA).CopyTo(input); ByteSpan roundOutput = hmac.ComputeHash(input); if (roundOutput.Length > writer.Length) { roundOutput = roundOutput.Slice(0, writer.Length); } roundOutput.CopyTo(writer); writer = writer.Slice(roundOutput.Length); } } }
/// <summary> /// Validates the authentication tag against the provided additional /// data, then decrypts the cipher text returning the original /// plaintext. /// </summary> /// <param name="nonce"> /// The unique value used to seal this message /// </param> /// <param name="ciphertext"> /// Combined ciphertext and authentication tag /// </param> /// <param name="associatedData"> /// Additional data used to authenticate the message /// </param> /// <param name="output"> /// On successful validation and decryprion, Open writes the original /// plaintext to output. Must contain enough space to hold /// `ciphertext.Length - CiphertextOverhead` bytes. /// </param> /// <returns> /// True if the data was validated and successfully decrypted. /// Otherwise, false. /// </returns> public bool Open(ByteSpan output, ByteSpan nonce, ByteSpan ciphertext, ByteSpan associatedData) { if (nonce.Length != NonceSize) { throw new ArgumentException("Invalid nonce size", nameof(nonce)); } if (ciphertext.Length < CiphertextOverhead) { throw new ArgumentException("Invalid ciphertext size", nameof(ciphertext)); } else if (output.Length < ciphertext.Length - CiphertextOverhead) { throw new ArgumentException("Invalid output size", nameof(output)); } // Split ciphertext into actual ciphertext and authentication // tag components. ByteSpan authenticationTag = ciphertext.Slice(ciphertext.Length - TagSize); ciphertext = ciphertext.Slice(0, ciphertext.Length - TagSize); // Create the initial counter block nonce.CopyTo(this.blockJ_); // Verify the tags match GenerateAuthenticationTag(this.blockScratch_, ciphertext, associatedData); if (0 == Const.ConstantCompareSpans(this.blockScratch_, authenticationTag)) { return(false); } // Decrypt the cipher text to output GCTR(output, this.blockJ_, 2, ciphertext); return(true); }
/// <summary> /// Create a new instance of the AES128_GCM record protection /// </summary> /// <param name="masterSecret">Shared secret</param> /// <param name="serverRandom">Server random data</param> /// <param name="clientRandom">Client random data</param> public Aes128GcmRecordProtection(ByteSpan masterSecret, ByteSpan serverRandom, ByteSpan clientRandom) { ByteSpan combinedRandom = new byte[serverRandom.Length + clientRandom.Length]; serverRandom.CopyTo(combinedRandom); clientRandom.CopyTo(combinedRandom.Slice(serverRandom.Length)); // Expand master_secret to encryption keys const int ExpandedSize = 0 + 0 // mac_key_length + 0 // mac_key_length + Aes128Gcm.KeySize // enc_key_length + Aes128Gcm.KeySize // enc_key_length + ImplicitNonceSize // fixed_iv_length + ImplicitNonceSize // fixed_iv_length ; ByteSpan expandedKey = new byte[ExpandedSize]; PrfSha256.ExpandSecret(expandedKey, masterSecret, PrfLabel.KEY_EXPANSION, combinedRandom); ByteSpan clientWriteKey = expandedKey.Slice(0, Aes128Gcm.KeySize); ByteSpan serverWriteKey = expandedKey.Slice(Aes128Gcm.KeySize, Aes128Gcm.KeySize); this.clientWriteIV = expandedKey.Slice(2 * Aes128Gcm.KeySize, ImplicitNonceSize); this.serverWriteIV = expandedKey.Slice(2 * Aes128Gcm.KeySize + ImplicitNonceSize, ImplicitNonceSize); this.serverWriteCipher = new Aes128Gcm(serverWriteKey); this.clientWriteCipher = new Aes128Gcm(clientWriteKey); }
/// <inheritdoc /> public void EncodeServerKeyExchangeMessage(ByteSpan output, object privateKey) { RSA rsaPrivateKey = privateKey as RSA; if (rsaPrivateKey == null) { throw new ArgumentException("Invalid private key", nameof(privateKey)); } output[0] = (byte)ECCurveType.NamedCurve; output.WriteBigEndian16((ushort)NamedCurve.x25519, 1); output[3] = (byte)X25519.KeySize; X25519.Func(output.Slice(4, X25519.KeySize), this.privateAgreementKey); // Hash the key parameters byte[] paramterDigest = this.sha256.ComputeHash(output.GetUnderlyingArray(), output.Offset, 4 + X25519.KeySize); // Sign the paramter digest RSAPKCS1SignatureFormatter signer = new RSAPKCS1SignatureFormatter(rsaPrivateKey); signer.SetHashAlgorithm("SHA256"); ByteSpan signature = signer.CreateSignature(paramterDigest); Debug.Assert(signature.Length == rsaPrivateKey.KeySize / 8); output[4 + X25519.KeySize] = (byte)HashAlgorithm.Sha256; output[5 + X25519.KeySize] = (byte)SignatureAlgorithm.RSA; output.Slice(6 + X25519.KeySize).WriteBigEndian16((ushort)signature.Length); signature.CopyTo(output.Slice(8 + X25519.KeySize)); }
/// <summary> /// Encode a HelloVerifyRequest payload to wire format /// </summary> /// <param name="peerAddress">Address of the remote peer</param> /// <param name="hmac">Listener HMAC signature provider</param> public static void Encode(ByteSpan span, EndPoint peerAddress, HMAC hmac) { ByteSpan cookie = ComputeAddressMac(peerAddress, hmac); span.WriteBigEndian16((ushort)ProtocolVersion.DTLS1_2); span[2] = (byte)CookieSize; cookie.CopyTo(span.Slice(3)); }
private static void CopyMaybeOverlappingSpans(ByteSpan output, ByteSpan input) { // Early out if the ranges `output` is equal to `input` if (output.GetUnderlyingArray() == input.GetUnderlyingArray()) { if (output.Offset == input.Offset && output.Length == input.Length) { return; } } input.CopyTo(output); }
/// <summary> /// Encode a certificate to wire formate /// </summary> public static ByteSpan Encode(X509Certificate2 certificate) { ByteSpan certData = certificate.GetRawCertData(); int totalSize = certData.Length + 3 + 3; ByteSpan result = new byte[totalSize]; ByteSpan writer = result; writer.WriteBigEndian24((uint)certData.Length + 3); writer = writer.Slice(3); writer.WriteBigEndian24((uint)certData.Length); writer = writer.Slice(3); certData.CopyTo(writer); return(result); }
private static void EncryptPlaintext(ByteSpan output, ByteSpan input, ref Record record, Aes128Gcm cipher, ByteSpan writeIV) { Debug.Assert(output.Length >= GetEncryptedSizeImpl(input.Length)); // Build GCM nonce (authenticated data) ByteSpan nonce = new byte[ImplicitNonceSize + ExplicitNonceSize]; writeIV.CopyTo(nonce); nonce.WriteBigEndian16(record.Epoch, ImplicitNonceSize); nonce.WriteBigEndian48(record.SequenceNumber, ImplicitNonceSize + 2); // Serialize record as additional data Record plaintextRecord = record; plaintextRecord.Length = (ushort)input.Length; ByteSpan associatedData = new byte[Record.Size]; plaintextRecord.Encode(associatedData); cipher.Seal(output, nonce, input, associatedData); }
/// <summary> /// Encryptes the specified plaintext and generates an authentication /// tag for the provided additional data. Returns the byte array /// containg both the ciphertext and authentication tag. /// </summary> /// <param name="output"> /// Array in which to encode the encrypted ciphertext and /// authentication tag. This array must be large enough to hold /// `plaintext.Lengh + CiphertextOverhead` bytes. /// </param> /// <param name="nonce">Unique value for this message</param> /// <param name="plaintext">Plaintext data to encrypt</param> /// <param name="associatedData"> /// Additional data used to authenticate the message /// </param> public void Seal(ByteSpan output, ByteSpan nonce, ByteSpan plaintext, ByteSpan associatedData) { if (nonce.Length != NonceSize) { throw new ArgumentException("Invalid nonce size", nameof(nonce)); } if (output.Length < plaintext.Length + CiphertextOverhead) { throw new ArgumentException("Invalid output size", nameof(output)); } // Create the initial counter block nonce.CopyTo(this.blockJ_); // Encrypt the plaintext to output GCTR(output, this.blockJ_, 2, plaintext); // Generate and append the authentication tag int tagOffset = plaintext.Length; GenerateAuthenticationTag(output.Slice(tagOffset), output.Slice(0, tagOffset), associatedData); }
/// <summary> /// Parse a Handshake Certificate payload from wire format /// </summary> /// <returns>True if we successfully decode the Certificate message. Otherwise false</returns> public static bool Parse(out X509Certificate2 certificate, ByteSpan span) { certificate = null; if (span.Length < 6) { return(false); } uint totalSize = span.ReadBigEndian24(); span = span.Slice(3); if (span.Length < totalSize) { return(false); } uint certificateSize = span.ReadBigEndian24(); span = span.Slice(3); if (span.Length < certificateSize) { return(false); } byte[] rawData = new byte[certificateSize]; span.CopyTo(rawData, 0); try { certificate = new X509Certificate2(rawData); } catch (Exception) { return(false); } return(true); }
public void ClientEncryptionCanoverlap() { using (Aes128GcmRecordProtection recordProtection = new Aes128GcmRecordProtection(this.masterSecret, this.serverRandom, this.clientRandom)) { ByteSpan messageAsBytes = Encoding.UTF8.GetBytes(TestMessage); Record record = new Record(); record.ContentType = ContentType.ApplicationData; record.Epoch = 1; record.SequenceNumber = 124; record.Length = (ushort)recordProtection.GetEncryptedSize(messageAsBytes.Length); ByteSpan encrypted = new byte[record.Length]; messageAsBytes.CopyTo(encrypted); recordProtection.EncryptClientPlaintext(encrypted, encrypted.Slice(0, messageAsBytes.Length), ref record); ByteSpan plaintext = encrypted.Slice(0, recordProtection.GetDecryptedSize(record.Length)); bool couldDecrypt = recordProtection.DecryptCiphertextFromClient(plaintext, encrypted, ref record); Assert.IsTrue(couldDecrypt); Assert.AreEqual(messageAsBytes.Length, plaintext.Length); Assert.AreEqual(TestMessage, Encoding.UTF8.GetString(plaintext.GetUnderlyingArray(), plaintext.Offset, plaintext.Length)); } }
// Multiply two Galois field elements `X` and `Y` together and store // the result in `X` such that at the end of the function: // X = X·Y static void MultiplyGF128Elements(ByteSpan X, ByteSpan Y, ByteSpan scratchZ, ByteSpan scratchV) { Debug.Assert(X.Length == 16); Debug.Assert(Y.Length == 16); Debug.Assert(scratchZ.Length == 16); Debug.Assert(scratchV.Length == 16); // Galois (finite) fields represented by GF(p) define a set of // closed algebraic operations. For AES128_GCM we'll be dealing // with the GF(2^128) field. // // We treat each incoming 16 byte block as a polynomial in field // and define multiplication between two polynomials as the // polynomial product reduced by (mod) the field polynomial: // 1 + x + x^2 + x^7 + x^128 // // Field polynomials are represented by a 128 bit string. Bit n is // the coefficient of the x^n term. We use little-endian bit // ordering (not to be confused with byte ordering) for these // coefficients. E.g. X[0] & 0x00000001 represents the 7th bit in // the bit string defined by X, _not_ the 0th bit. // // What follows is a modified version of the "peasant's algorithm" // to multiply two numbers: // // Z contains the accumulated product // V is a copy of Y (so we can modify it via shifting). // // We calculate Z = X·V as follows // We loop through each of the 128 bits in X maintaining the // following loop invariant: X·V + Z = the final product // // On each iteration `ii`: // // If the `ii`th bit of `X` is set, add the add the polynomial // in `V` to `X`: `X[n] = X[n] ^ V[n]` // // Double V (Shift one bit right since we're storing little // endian bit). This has the effect of multiplying V by the // polynomial `x`. We track the unrepresentable coefficient // of `x^128` by storing the most significant bit before the // shift `V[15] >> 7` as `carry` // // Check if we've overflowed our multiplication. If overflow // occurred, there will be a non-zero coefficient for the // `x^128` term in the step above `carry` // // If we have overflowed, our polynomial is exactly of degree // 129 (since we're only multiplying by `x`). We reduce the // polynomial back into degree 128 by adding our field's // irreducible polynomial: 1 + x + x^2 + x^7 + x^128. This // reduction cancels out the x^128 term (x^128 + x^128 in GF(2) // is zero). Therefore this modulo can be achieved by simply // adding the irreducible polynomial to the new value of `V`. The // irreducible polynomial is represented by the bit string: // `11100001` followed by 120 `0`s. We can add this value to `V` // by: `V[0] = V[0] ^ 0xE1`. SetSpanToZeros(scratchZ); X.CopyTo(scratchV); for (int ii = 0; ii != 128; ++ii) { int bitIndex = 7 - (ii % 8); if ((Y[ii / 8] & (1 << bitIndex)) != 0) { for (int jj = 0; jj != 16; ++jj) { scratchZ[jj] ^= scratchV[jj]; } } bool carry = false; for (int jj = 0; jj != 16; ++jj) { bool newCarry = (scratchV[jj] & 0x01) != 0; scratchV[jj] >>= 1; if (carry) { scratchV[jj] |= 0x80; } carry = newCarry; } if (carry) { scratchV[0] ^= 0xE1; } } scratchZ.CopyTo(X); }
// The FieldElement code below is ported from the original // public domain reference implemtation of X25519 // by D. J. Bernstien // // See: https://cr.yp.to/ecdh.html private static void InternalFunc(ByteSpan output, ByteSpan scalar, ByteSpan point) { if (output.Length != KeySize) { throw new ArgumentException("Invalid output size", nameof(output)); } else if (scalar.Length != KeySize) { throw new ArgumentException("Invalid scalar size", nameof(scalar)); } else if (point.Length != KeySize) { throw new ArgumentException("Invalid point size", nameof(point)); } // copy the scalar so we can properly mask it ByteSpan maskedScalar = new byte[32]; scalar.CopyTo(maskedScalar); maskedScalar[0] &= 248; maskedScalar[31] &= 127; maskedScalar[31] |= 64; FieldElement x1 = FieldElement.FromBytes(point); FieldElement x2 = FieldElement.One(); FieldElement x3 = x1; FieldElement z2 = FieldElement.Zero(); FieldElement z3 = FieldElement.One(); FieldElement tmp0 = new FieldElement(); FieldElement tmp1 = new FieldElement(); int swap = 0; for (int pos = 254; pos >= 0; --pos) { int b = (int)maskedScalar[pos / 8] >> (int)(pos % 8); b &= 1; swap ^= b; FieldElement.ConditionalSwap(ref x2, ref x3, swap); FieldElement.ConditionalSwap(ref z2, ref z3, swap); swap = b; FieldElement.Sub(ref tmp0, ref x3, ref z3); FieldElement.Sub(ref tmp1, ref x2, ref z2); FieldElement.Add(ref x2, ref x2, ref z2); FieldElement.Add(ref z2, ref x3, ref z3); FieldElement.Multiply(ref z3, ref tmp0, ref x2); FieldElement.Multiply(ref z2, ref z2, ref tmp1); FieldElement.Square(ref tmp0, ref tmp1); FieldElement.Square(ref tmp1, ref x2); FieldElement.Add(ref x3, ref z3, ref z2); FieldElement.Sub(ref z2, ref z3, ref z2); FieldElement.Multiply(ref x2, ref tmp1, ref tmp0); FieldElement.Sub(ref tmp1, ref tmp1, ref tmp0); FieldElement.Square(ref z2, ref z2); FieldElement.Multiply121666(ref z3, ref tmp1); FieldElement.Square(ref x3, ref x3); FieldElement.Add(ref tmp0, ref tmp0, ref z3); FieldElement.Multiply(ref z3, ref x1, ref z2); FieldElement.Multiply(ref z2, ref tmp1, ref tmp0); } FieldElement.ConditionalSwap(ref x2, ref x3, swap); FieldElement.ConditionalSwap(ref z2, ref z3, swap); FieldElement.Invert(ref z2, ref z2); FieldElement.Multiply(ref x2, ref x2, ref z2); x2.CopyTo(output); }
/// <summary> /// Process an incoming Handshake protocol message /// </summary> /// <param name="record">Parent record</param> /// <param name="message">Record payload</param> /// <returns> /// True if further processing of the underlying datagram /// should be continues. Otherwise, false. /// </returns> private bool ProcessHandshake(ref Record record, ByteSpan message) { // Each record may have multiple Handshake messages while (message.Length > 0) { ByteSpan originalPayload = message; Handshake handshake; if (!Handshake.Parse(out handshake, message)) { this.logger.WriteError("Dropping malformed handshake message"); return(false); } message = message.Slice(Handshake.Size); if (message.Length < handshake.Length) { this.logger.WriteError($"Dropping malformed handshake message: AvailableBytes({message.Length}) Size({handshake.Length})"); return(false); } originalPayload = originalPayload.Slice(0, (int)(Handshake.Size + handshake.Length)); ByteSpan payload = originalPayload.Slice(Handshake.Size); message = message.Slice((int)handshake.Length); // We only support fragmented Certificate messages // from the server if (handshake.MessageType != HandshakeType.Certificate && (handshake.FragmentOffset != 0 || handshake.FragmentLength != handshake.Length)) { this.logger.WriteError($"Dropping fragmented handshake message Type({handshake.MessageType}) Offset({handshake.FragmentOffset}) FragmentLength({handshake.FragmentLength}) Length({handshake.Length})"); continue; } switch (handshake.MessageType) { case HandshakeType.HelloVerifyRequest: if (this.nextEpoch.State != HandshakeState.ExpectingServerHello) { this.logger.WriteError($"Dropping unexpected HelloVerifyRequest handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 0) { this.logger.WriteError($"Dropping bad-sequence HelloVerifyRequest MessageSequence({handshake.MessageSequence})"); continue; } HelloVerifyRequest helloVerifyRequest; if (!HelloVerifyRequest.Parse(out helloVerifyRequest, payload)) { this.logger.WriteError("Dropping malformed HelloVerifyRequest handshake message"); continue; } // Save the cookie this.nextEpoch.Cookie = new byte[helloVerifyRequest.Cookie.Length]; helloVerifyRequest.Cookie.CopyTo(this.nextEpoch.Cookie); // Restart the handshake this.SendClientHello(); break; case HandshakeType.ServerHello: if (this.nextEpoch.State != HandshakeState.ExpectingServerHello) { this.logger.WriteError($"Dropping unexpected ServerHello handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 1) { this.logger.WriteError($"Dropping bad-sequence ServerHello MessageSequence({handshake.MessageSequence})"); continue; } ServerHello serverHello; if (!ServerHello.Parse(out serverHello, payload)) { this.logger.WriteError("Dropping malformed ServerHello message"); continue; } switch (serverHello.CipherSuite) { case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: this.nextEpoch.Handshake = new X25519EcdheRsaSha256(this.random); break; default: this.logger.WriteError($"Dropping malformed ServerHello message. Unsupported CipherSuite({serverHello.CipherSuite})"); continue; } // Save server parameters this.nextEpoch.SelectedCipherSuite = serverHello.CipherSuite; serverHello.Random.CopyTo(this.nextEpoch.ServerRandom); this.nextEpoch.State = HandshakeState.ExpectingCertificate; this.nextEpoch.CertificateFragments.Clear(); this.nextEpoch.CertificatePayload = ByteSpan.Empty; // Append ServerHelllo message to the verification stream this.nextEpoch.VerificationStream.Write( originalPayload.GetUnderlyingArray() , originalPayload.Offset , originalPayload.Length ); break; case HandshakeType.Certificate: if (this.nextEpoch.State != HandshakeState.ExpectingCertificate) { this.logger.WriteError($"Dropping unexpected Certificate handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 2) { this.logger.WriteError($"Dropping bad-sequence Certificate MessageSequence({handshake.MessageSequence})"); continue; } // If this is a fragmented message if (handshake.FragmentLength != handshake.Length) { if (this.nextEpoch.CertificatePayload.Length != handshake.Length) { this.nextEpoch.CertificatePayload = new byte[handshake.Length]; this.nextEpoch.CertificateFragments.Clear(); } // Add this fragment payload.CopyTo(this.nextEpoch.CertificatePayload.Slice((int)handshake.FragmentOffset, (int)handshake.FragmentLength)); this.nextEpoch.CertificateFragments.Add(new FragmentRange { Offset = (int)handshake.FragmentOffset, Length = (int)handshake.FragmentLength }); this.nextEpoch.CertificateFragments.Sort((FragmentRange lhs, FragmentRange rhs) => { return(lhs.Offset.CompareTo(rhs.Offset)); }); // Have we completed the message? int currentOffset = 0; bool valid = true; foreach (FragmentRange range in this.nextEpoch.CertificateFragments) { if (range.Offset != currentOffset) { valid = false; break; } currentOffset += range.Length; } if (currentOffset != this.nextEpoch.CertificatePayload.Length) { valid = false; } // Still waiting on more fragments? if (!valid) { continue; } // Replace the message payload, and continue this.nextEpoch.CertificateFragments.Clear(); payload = this.nextEpoch.CertificatePayload; } X509Certificate2 certificate; if (!Certificate.Parse(out certificate, payload)) { this.logger.WriteError("Dropping malformed Certificate message"); continue; } // Verify the certificate is authenticate if (!this.serverCertificates.Contains(certificate)) { this.logger.WriteError("Dropping malformed Certificate message: Certificate not authentic"); continue; } RSA publicKey = certificate.PublicKey.Key as RSA; if (publicKey == null) { this.logger.WriteError("Dropping malfomed Certificate message: Certificate is not RSA signed"); continue; } // Add the final Certificate message to the verification stream Handshake fullCertificateHandhake = handshake; fullCertificateHandhake.FragmentOffset = 0; fullCertificateHandhake.FragmentLength = fullCertificateHandhake.Length; byte[] serializedCertificateHandshake = new byte[Handshake.Size]; fullCertificateHandhake.Encode(serializedCertificateHandshake); this.nextEpoch.VerificationStream.Write(serializedCertificateHandshake, 0, serializedCertificateHandshake.Length); this.nextEpoch.VerificationStream.Write(payload.GetUnderlyingArray(), payload.Offset, payload.Length); this.nextEpoch.ServerPublicKey = publicKey; this.nextEpoch.State = HandshakeState.ExpectingServerKeyExchange; break; case HandshakeType.ServerKeyExchange: if (this.nextEpoch.State != HandshakeState.ExpectingServerKeyExchange) { this.logger.WriteError($"Dropping unexpected ServerKeyExchange handshake message State({this.nextEpoch.State})"); continue; } else if (this.nextEpoch.ServerPublicKey == null) { ///NOTE(mendsley): This _should_ not /// happen on a well-formed client Debug.Assert(false, "How are we processing a ServerKeyExchange message without a server public key?"); this.logger.WriteError($"Dropping unexpected ServerKeyExchange handshake message: No server public key"); continue; } else if (this.nextEpoch.Handshake == null) { ///NOTE(mendsley): This _should_ not /// happen on a well-formed client Debug.Assert(false, "How did we receive a ServerKeyExchange message without a handshake instance?"); this.logger.WriteError($"Dropping unexpected ServerKeyExchange handshake message: No key agreement interface"); continue; } else if (handshake.MessageSequence != 3) { this.logger.WriteError($"Dropping bad-sequence ServerKeyExchange MessageSequence({handshake.MessageSequence})"); continue; } ByteSpan sharedSecret = new byte[this.nextEpoch.Handshake.SharedKeySize()]; if (!this.nextEpoch.Handshake.VerifyServerMessageAndGenerateSharedKey(sharedSecret, payload, this.nextEpoch.ServerPublicKey)) { this.logger.WriteError("Dropping malformed ServerKeyExchangeMessage"); return(false); } // Generate the session master secret ByteSpan randomSeed = new byte[2 * Random.Size]; this.nextEpoch.ClientRandom.CopyTo(randomSeed); this.nextEpoch.ServerRandom.CopyTo(randomSeed.Slice(Random.Size)); const int MasterSecretSize = 48; ByteSpan masterSecret = new byte[MasterSecretSize]; PrfSha256.ExpandSecret( masterSecret , sharedSecret , PrfLabel.MASTER_SECRET , randomSeed ); // Create record protection for the upcoming epoch switch (this.nextEpoch.SelectedCipherSuite) { case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: this.nextEpoch.RecordProtection = new Aes128GcmRecordProtection( masterSecret , this.nextEpoch.ServerRandom , this.nextEpoch.ClientRandom ); break; default: ///NOTE(mendsley): this _should_ not /// happen on a well-formed client. Debug.Assert(false, "SeverHello processing already approved this ciphersuite"); this.logger.WriteError($"Dropping malformed ServerKeyExchangeMessage: Could not create record protection"); return(false); } this.nextEpoch.State = HandshakeState.ExpectingServerHelloDone; this.nextEpoch.MasterSecret = masterSecret; // Append ServerKeyExchange to the verification stream this.nextEpoch.VerificationStream.Write( originalPayload.GetUnderlyingArray() , originalPayload.Offset , originalPayload.Length ); break; case HandshakeType.ServerHelloDone: if (this.nextEpoch.State != HandshakeState.ExpectingServerHelloDone) { this.logger.WriteError($"Dropping unexpected ServerHelloDone handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 4) { this.logger.WriteError($"Dropping bad-sequence ServerHelloDone MessageSequence({handshake.MessageSequence})"); continue; } this.nextEpoch.State = HandshakeState.ExpectingChangeCipherSpec; // Append ServerHelloDone to the verification stream this.nextEpoch.VerificationStream.Write( originalPayload.GetUnderlyingArray() , originalPayload.Offset , originalPayload.Length ); this.SendClientKeyExchangeFlight(false); break; case HandshakeType.Finished: if (this.nextEpoch.State != HandshakeState.ExpectingFinished) { this.logger.WriteError($"Dropping unexpected Finished handshake message State({this.nextEpoch.State})"); continue; } else if (payload.Length != Finished.Size) { this.logger.WriteError($"Dropping malformed Finished handshake message Size({payload.Length})"); continue; } else if (handshake.MessageSequence != 7) { this.logger.WriteError($"Dropping bad-sequence Finished MessageSequence({handshake.MessageSequence})"); continue; } // Verify the digest from the server if (1 != Crypto.Const.ConstantCompareSpans(payload, this.nextEpoch.ServerVerification)) { this.logger.WriteError("Dropping non-verified Finished handshake message"); return(false); } ++this.nextEpoch.Epoch; this.nextEpoch.State = HandshakeState.Established; this.nextEpoch.NextPacketResendTime = DateTime.MinValue; this.nextEpoch.ServerVerification.SecureClear(); this.nextEpoch.MasterSecret.SecureClear(); this.FlushQueuedApplicationData(); break; // Drop messages we do not support case HandshakeType.CertificateRequest: case HandshakeType.HelloRequest: this.logger.WriteError($"Dropping unsupported handshake message MessageType({handshake.MessageType})"); break; // Drop messages that originate from the client case HandshakeType.ClientHello: case HandshakeType.ClientKeyExchange: case HandshakeType.CertificateVerify: this.logger.WriteError($"Dropping client handshake message MessageType({handshake.MessageType})"); break; } } return(true); }
/// <summary> /// Handle an incoming datagram /// </summary> /// <param name="span">Bytes of the datagram</param> private void HandleReceive(ByteSpan span) { // Each incoming packet may contain multiple DTLS // records while (span.Length > 0) { Record record; if (!Record.Parse(out record, span)) { this.logger.WriteError("Dropping malformed record"); return; } span = span.Slice(Record.Size); if (span.Length < record.Length) { this.logger.WriteError($"Dropping malformed record. Length({record.Length}) Available Bytes({span.Length})"); return; } ByteSpan recordPayload = span.Slice(0, record.Length); span = span.Slice(record.Length); // Early out and drop ApplicationData records if (record.ContentType == ContentType.ApplicationData && this.nextEpoch.State != HandshakeState.Established) { this.logger.WriteError("Dropping ApplicationData record. Cannot process yet"); continue; } // Drop records from a different epoch if (record.Epoch != this.epoch) { this.logger.WriteError($"Dropping bad-epoch record. RecordEpoch({record.Epoch}) Epoch({this.epoch})"); continue; } // Prevent replay attacks by dropping records // we've already processed int windowIndex = (int)(this.currentEpoch.NextExpectedSequence - record.SequenceNumber - 1); ulong windowMask = 1ul << windowIndex; if (record.SequenceNumber < this.currentEpoch.NextExpectedSequence) { if (windowIndex >= 64) { this.logger.WriteError($"Dropping too-old record: Sequnce({record.SequenceNumber}) Expected({this.currentEpoch.NextExpectedSequence})"); continue; } if ((this.currentEpoch.PreviousSequenceWindowBitmask & windowMask) != 0) { this.logger.WriteError("Dropping duplicate record"); continue; } } // Verify record authenticity int decryptedSize = this.currentEpoch.RecordProtection.GetDecryptedSize(recordPayload.Length); ByteSpan decryptedPayload = recordPayload.ReuseSpanIfPossible(decryptedSize); if (!this.currentEpoch.RecordProtection.DecryptCiphertextFromServer(decryptedPayload, recordPayload, ref record)) { this.logger.WriteError("Dropping non-authentic record"); return; } recordPayload = decryptedPayload; // Update out sequence number bookkeeping if (record.SequenceNumber >= this.currentEpoch.NextExpectedSequence) { int windowShift = (int)(record.SequenceNumber + 1 - this.currentEpoch.NextExpectedSequence); this.currentEpoch.PreviousSequenceWindowBitmask <<= windowShift; this.currentEpoch.NextExpectedSequence = record.SequenceNumber + 1; } else { this.currentEpoch.PreviousSequenceWindowBitmask |= windowMask; } switch (record.ContentType) { case ContentType.ChangeCipherSpec: if (this.nextEpoch.State != HandshakeState.ExpectingChangeCipherSpec) { this.logger.WriteError($"Dropping unexpected ChangeCipherSpec State({this.nextEpoch.State})"); break; } else if (this.nextEpoch.RecordProtection == null) { ///NOTE(mendsley): This _should_ not /// happen on a well-formed client. Debug.Assert(false, "How did we receive a ChangeCipherSpec message without a pending record protection instance?"); break; } if (!ChangeCipherSpec.Parse(recordPayload)) { this.logger.WriteError("Dropping malformed ChangeCipherSpec message"); break; } // Migrate to the next epoch this.epoch = this.nextEpoch.Epoch; this.currentEpoch.RecordProtection = this.nextEpoch.RecordProtection; this.currentEpoch.NextOutgoingSequence = this.nextEpoch.NextOutgoingSequence; this.currentEpoch.NextExpectedSequence = 1; this.currentEpoch.PreviousSequenceWindowBitmask = 0; this.nextEpoch.State = HandshakeState.ExpectingFinished; this.nextEpoch.SelectedCipherSuite = CipherSuite.TLS_NULL_WITH_NULL_NULL; this.nextEpoch.RecordProtection = null; this.nextEpoch.Handshake?.Dispose(); this.nextEpoch.Cookie = ByteSpan.Empty; this.nextEpoch.VerificationStream.SetLength(0); this.nextEpoch.ServerPublicKey = null; this.nextEpoch.ServerRandom.SecureClear(); this.nextEpoch.ClientRandom.SecureClear(); this.nextEpoch.MasterSecret.SecureClear(); break; case ContentType.Alert: this.logger.WriteError("Dropping unsupported alert record"); continue; case ContentType.Handshake: if (!ProcessHandshake(ref record, recordPayload)) { return; } break; case ContentType.ApplicationData: // Forward data to the application MessageReader reader = MessageReader.GetSized(recordPayload.Length); reader.Length = recordPayload.Length; recordPayload.CopyTo(reader.Buffer); base.HandleReceive(reader, recordPayload.Length); break; } } }