/// <summary> /// Performs the secure handshake so we can start sending messages. /// </summary> /// <returns></returns> private async Task HandshakeAsync() { // wait on semaphore await _handshakeSemaphore.WaitAsync(); // check if we got raced if (_serverEncryptionKey != null && !SecureUtils.HasTimeSlotExpired(_serverEncryptionKeyTimeSlot, false)) { return; } try { // create client key GenerateHandshakeKey(); // get certificate if we don't have it yet if (_serverCertificate == null) { // log #if DEBUG_SECURE Console.WriteLine($"[Secure] Handshake Requesting certificate..."); #endif // request certificate SecureHeader requestCertificate = new SecureHeader(SecureHeader.HeaderVersion, SecureMessageType.RequestCertificate); // send request Envelope respondCert = await _node.AskAsync(_address, new byte[0], _configuration.HandshakeTimeout, new Dictionary <string, object>() { { SecureHeader.HeaderName, requestCertificate.ToString() } }); // parse response header SecureHeader respondCertHeader = null; try { respondCertHeader = new SecureHeader(Encoding.UTF8.GetString(respondCert.Headers[SecureHeader.HeaderName] as byte[])); } catch (Exception ex) { throw new InvalidDataException("The certificate request response header was invalid", ex); } // check if the certificate response is an error or if it's an incorrect type if (respondCertHeader.Type == SecureMessageType.Error) { SecureErrorMsg errorMsg = respondCert.AsProtoBuf <SecureErrorMsg>(); throw new SecurityException($"{errorMsg.Message} ({errorMsg.Code})"); } else if (respondCertHeader.Type != SecureMessageType.RespondCertificate) { throw new InvalidDataException("The certificate request response header was invalid"); } // decode the certificate response and then check it has actual data SecureRespondCertificateMsg respondCertMsg = respondCert.AsProtoBuf <SecureRespondCertificateMsg>(); if (respondCertMsg.CertificateData == null) { throw new InvalidDataException("The certificate request response was invalid"); } // load certificate from response X509Certificate2 cert = new X509Certificate2(respondCertMsg.CertificateData); // validate it's allowed to act as this service if (_configuration.ValidateAddress) { // check extension is actually there if (cert.Extensions["HolonSecureServices"] == null) { throw new InvalidDataException("The service certificate has no claim for the invoked operation"); } // parse extension X509Extension servicesExt = cert.Extensions["HolonSecureServices"]; } else { #if DEBUG_SECURE Console.WriteLine($"[Secure] Handshake WARNING: Not validating services due to configuration!"); #endif } // validate that it's signed by ca authority if (_configuration.ValidateAuthority) { X509Chain chain = new X509Chain(); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; chain.ChainPolicy.ExtraStore.Add(_configuration.RootAuthority); chain.Build(cert); // get status X509ChainStatus status = chain.ChainStatus.First(); if (status.Status != X509ChainStatusFlags.UntrustedRoot && status.Status != X509ChainStatusFlags.NoError) { throw new InvalidDataException("The service certificate is not signed by the root authority"); } } else { #if DEBUG_SECURE Console.WriteLine($"[Secure] Handshake WARNING: Not validating authority due to configuration!"); #endif } // log #if DEBUG_SECURE Console.WriteLine($"[Secure] Handshake Got certifcate {cert.Subject} ({cert.Thumbprint})"); #endif _serverCertificate = cert; } // log #if DEBUG_SECURE Console.WriteLine($"[Secure] Handshake Requesting key... FirstTime: {_serverEncryptionKey == null}"); #endif // key request SecureRequestKeyMsg requestKeyMsg = new SecureRequestKeyMsg() { HandshakeIV = _handshakeEncryptionIV, HandshakeKey = _handshakeEncryptionKey }; SecureHeader requestKey = new SecureHeader(SecureHeader.HeaderVersion, SecureMessageType.RequestKey); // send request Envelope respondKey = null; using (RSA rsa = _serverCertificate.GetRSAPublicKey()) { using (MemoryStream ms = new MemoryStream()) { // serialize to stream Serializer.Serialize(ms, requestKeyMsg); // encrypt with server certificate byte[] keyRequestBody = rsa.Encrypt(ms.ToArray(), RSAEncryptionPadding.Pkcs1); respondKey = await _node.AskAsync(_address, keyRequestBody, _configuration.HandshakeTimeout, new Dictionary <string, object>() { { SecureHeader.HeaderName, requestKey.ToString() } }); } } // parse response SecureHeader respondKeyHeader = null; try { respondKeyHeader = new SecureHeader(Encoding.UTF8.GetString(respondKey.Headers[SecureHeader.HeaderName] as byte[])); } catch (Exception ex) { throw new InvalidDataException("The key request response header was invalid", ex); } // check if the key response is an error or if it's an incorrect type if (respondKeyHeader.Type == SecureMessageType.Error) { SecureErrorMsg errorMsg = respondKey.AsProtoBuf <SecureErrorMsg>(); throw new SecurityException($"{errorMsg.Message} ({errorMsg.Code})"); } else if (respondKeyHeader.Type != SecureMessageType.RespondKey) { throw new InvalidDataException("The key request response header was invalid"); } // try and decrypt using (MemoryStream decryptedStream = new MemoryStream()) { using (Aes aes = Aes.Create()) { aes.Key = _handshakeEncryptionKey; aes.IV = _handshakeEncryptionIV; using (CryptoStream decryptStream = new CryptoStream(respondKey.AsStream(), aes.CreateDecryptor(), CryptoStreamMode.Read)) { decryptStream.CopyTo(decryptedStream); } } // seek to beginning decryptedStream.Seek(0, SeekOrigin.Begin); // deserialize SecureRespondKeyMsg respondKeyMsg = Serializer.Deserialize <SecureRespondKeyMsg>(decryptedStream); // validate key if (respondKeyMsg.ServerKey == null || respondKeyMsg.ServerNonce == null || respondKeyMsg.ServerKey.Length != 16) { throw new InvalidDataException("The secure key is invalid"); } // log #if DEBUG_SECURE Console.WriteLine($"[Secure] Handshake Got key Timeslot: {respondKeyMsg.KeyTimeSlot} Nonce: {BitConverter.ToString(respondKeyMsg.ServerNonce).Replace("-", "")}"); #endif // set server encryption key _serverEncryptionKey = respondKeyMsg.ServerKey; _serverNonce = respondKeyMsg.ServerNonce; _serverEncryptionKeyTimeSlot = respondKeyMsg.KeyTimeSlot; } } finally { // make sure to release semaphore no matter what _handshakeSemaphore.Release(); } }
/// <summary> /// Handles handshake and encrypted envelopes. /// </summary> /// <param name="envelope">The envelope.</param> /// <returns></returns> public async Task <bool> HandleAsync(Envelope envelope) { // check for secure header SecureHeader secureHeader = null; try { if (envelope.Headers.ContainsKey(SecureHeader.HEADER_NAME)) { secureHeader = new SecureHeader(Encoding.UTF8.GetString(envelope.Headers[SecureHeader.HEADER_NAME] as byte[])); } } catch (Exception) { if (envelope.ID != Guid.Empty) { await ReplyErrorAsync(envelope, new SecureErrorMsg() { Code = "ProtocolInvalid", Message = "The secure message header format is invalid" }); } return(false); } // check if it's a secure message or not if (secureHeader != null && envelope.ID != Guid.Empty) { if (secureHeader.Type == SecureMessageType.RequestCertificate) { #if DEBUG_SECURERPC Console.WriteLine($"[SecureFilter] {nameof(RpcSecureMessageType.RequestCertificate)}"); #endif // build response SecureRespondCertificateMsg respondCertificateMsg = new SecureRespondCertificateMsg(); respondCertificateMsg.CertificateData = _certificate.Export(X509ContentType.Cert); // build reply using (MemoryStream ms = new MemoryStream()) { Serializer.Serialize(ms, respondCertificateMsg); // reply await envelope.Node.ReplyAsync(envelope.ReplyTo, envelope.ID, ms.ToArray(), new Dictionary <string, object>() { { SecureHeader.HEADER_NAME, new SecureHeader(SecureHeader.HEADER_VERSION, SecureMessageType.RespondCertificate).ToString() } }); } return(false); } else if (secureHeader.Type == SecureMessageType.RequestKey) { // decrypt byte[] decryptedBody = null; using (RSA rsa = _certificate.GetRSAPrivateKey()) { decryptedBody = rsa.Decrypt(envelope.Body, RSAEncryptionPadding.Pkcs1); } // deserialize key request SecureRequestKeyMsg requestKeyMsg = null; using (MemoryStream ms = new MemoryStream(decryptedBody)) { requestKeyMsg = Serializer.Deserialize <SecureRequestKeyMsg>(ms); } // validate that the data is there and is the correct size, if not send an error back if (requestKeyMsg.HandshakeIV == null || requestKeyMsg.HandshakeKey == null || requestKeyMsg.HandshakeIV.Length != 16 || requestKeyMsg.HandshakeKey.Length != 16) { await ReplyErrorAsync(envelope, new SecureErrorMsg() { Code = "ProtocolInvalid", Message = "The request key data was invalid" }); return(false); } // generate random nonce byte[] nonceBytes = new byte[16]; using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) { rng.GetBytes(nonceBytes); } // process key request long timeSlot = SecureUtils.GetNextTimeSlot(); byte[] keyBytes = SecureUtils.GenerateKey(nonceBytes, timeSlot, _secret); // log #if DEBUG_SECURERPC Console.WriteLine($"[SecureFilter] {nameof(RpcSecureMessageType.RequestKey)} TimeSlot: {timeSlot} Nonce: {BitConverter.ToString(nonceBytes).Replace("-", "")}"); #endif // build response SecureRespondKeyMsg respondKeyMsg = new SecureRespondKeyMsg(); respondKeyMsg.ServerNonce = nonceBytes; respondKeyMsg.ServerKey = keyBytes; respondKeyMsg.KeyTimeSlot = timeSlot; // encode and encrypt byte[] respondKeyBody = null; using (Aes aes = Aes.Create()) { // setup aes aes.Key = requestKeyMsg.HandshakeKey; aes.IV = requestKeyMsg.HandshakeIV; // build body using (MemoryStream ms = new MemoryStream()) { // encrypt using client key using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write)) { Serializer.Serialize(cs, respondKeyMsg); } // get output respondKeyBody = ms.ToArray(); } } // reply await envelope.Node.ReplyAsync(envelope.ReplyTo, envelope.ID, respondKeyBody, new Dictionary <string, object>() { { SecureHeader.HEADER_NAME, new SecureHeader(SecureHeader.HEADER_VERSION, SecureMessageType.RespondKey).ToString() } }); return(false); } else if (secureHeader.Type == SecureMessageType.RequestMessage) { // deserialize key request SecureMessageMsg msg = null; using (MemoryStream ms = new MemoryStream(envelope.Body)) { msg = Serializer.Deserialize <SecureMessageMsg>(ms); } // validate the data is there and is correct length, if not send invalid data if (msg.Payload == null || msg.ServerNonce == null || msg.ServerNonce.Length != 16) { await ReplyErrorAsync(envelope, new SecureErrorMsg() { Code = "ProtocolInvalid", Message = "The request message data was invalid" }); return(false); } // log #if DEBUG_SECURERPC Console.WriteLine($"[SecureFilter] {nameof(RpcSecureMessageType.RequestMessage)} TimeSlot: {msg.KeyTimeSlot} Nonce: {BitConverter.ToString(msg.ServerNonce).Replace("-", "")}"); #endif // validate expiry of time slot if (SecureUtils.HasTimeSlotExpired(msg.KeyTimeSlot, true)) { await ReplyErrorAsync(envelope, new SecureErrorMsg() { Code = "KeyExpired", Message = "The secure message is encrypted with an outdated key" }); return(false); } // get key byte[] keyBytes = SecureUtils.GenerateKey(msg.ServerNonce, msg.KeyTimeSlot, _secret); using (MemoryStream decryptedPayloadStream = new MemoryStream()) { using (MemoryStream payloadStream = new MemoryStream(msg.Payload)) { using (Aes aes = Aes.Create()) { aes.Key = keyBytes; aes.IV = msg.ServerNonce; using (CryptoStream decryptStream = new CryptoStream(payloadStream, aes.CreateDecryptor(), CryptoStreamMode.Read)) { decryptStream.CopyTo(decryptedPayloadStream); } } } // modify body and set reply channel envelope.Body = decryptedPayloadStream.ToArray(); envelope.Channel = new SecureReplyChannel(envelope, keyBytes, msg.ServerNonce); return(true); } } else { // log #if DEBUG_SECURERPC Console.WriteLine($"[SecureFilter] Unimplemented message type!"); #endif await ReplyErrorAsync(envelope, new SecureErrorMsg() { Code = "ProtocolViolation", Message = "The message type is not relevant or is invalid" }); return(false); } } else { return(true); } }
/// <summary> /// Broadcasts the envelope message to the provided service address and waits for a response. /// </summary> /// <param name="message">The message.</param> /// <param name="timeout">The timeout to receive all replies.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public async Task <Envelope[]> BroadcastAsync(Message message, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)) { // perform handshake if we don't have our key yet or it has expired if (_serverEncryptionKey == null || SecureUtils.HasTimeSlotExpired(_serverEncryptionKeyTimeSlot, false)) { #if DEBUG_SECURE Console.WriteLine($"[Secure] InvokeOperation KeyNull: {_serverEncryptionKey == null} Expired: {SecureUtils.HasTimeSlotExpired(_serverEncryptionKeyTimeSlot, false)}"); #endif // perform handshake (partial if required) await HandshakeAsync(); } // add secure header if (message.Headers == null) { message.Headers = new Dictionary <string, object>(StringComparer.CurrentCultureIgnoreCase); } message.Headers[SecureHeader.HeaderName] = new SecureHeader(SecureHeader.HeaderVersion, SecureMessageType.RequestMessage).ToString(); message.Address = _address; message.Body = EncryptBody(message.Body); // send the envelope and wait for the response Envelope[] responses = await _node.BroadcastAsync(message, timeout, cancellationToken); return(responses.Select(response => { if (!response.Headers.ContainsKey(SecureHeader.HeaderName)) { throw new InvalidDataException("The secure service sent an invalid response"); } // decode header and take action depending on the response SecureHeader header = new SecureHeader(Encoding.UTF8.GetString(response.Headers[SecureHeader.HeaderName] as byte[])); try { if (header.Type == SecureMessageType.RespondMessage) { using (MemoryStream outputStream = new MemoryStream()) { // decrypt using (MemoryStream inputStream = new MemoryStream(response.Body)) { using (Aes aes = Aes.Create()) { aes.Key = _serverEncryptionKey; aes.IV = _serverNonce; using (CryptoStream decryptStream = new CryptoStream(inputStream, aes.CreateDecryptor(), CryptoStreamMode.Read)) { decryptStream.CopyTo(outputStream); } } } response.Body = outputStream.ToArray(); return response; } } else { return null; } } catch (Exception) { return null; } }).Where(e => e != null).ToArray()); }
/// <summary> /// Sends the envelope message to the provided service address and waits for a response. /// </summary> /// <param name="body">The body.</param> /// <param name="headers">The headers.</param> /// <param name="timeout">The timeout.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public async Task <Envelope> AskAsync(byte[] body, TimeSpan timeout, IDictionary <string, object> headers = null, CancellationToken cancellationToken = default(CancellationToken)) { // perform handshake if we don't have our key yet or it has expired if (_serverEncryptionKey == null || SecureUtils.HasTimeSlotExpired(_serverEncryptionKeyTimeSlot, false)) { #if DEBUG_SECURE Console.WriteLine($"[Secure] InvokeOperation KeyNull: {_serverEncryptionKey == null} Expired: {SecureUtils.HasTimeSlotExpired(_serverEncryptionKeyTimeSlot, false)}"); #endif // perform handshake (partial if required) await HandshakeAsync(); } // add secure header if (headers == null) { headers = new Dictionary <string, object>(StringComparer.CurrentCultureIgnoreCase); } headers[SecureHeader.HEADER_NAME] = new SecureHeader(SecureHeader.HEADER_VERSION, SecureMessageType.RequestMessage).ToString(); // send the envelope and wait for the response Envelope response = await _node.AskAsync(_address, EncryptBody(body), timeout, headers, cancellationToken); if (!response.Headers.ContainsKey(SecureHeader.HEADER_NAME)) { throw new InvalidDataException("The secure service sent an invalid response"); } // decode header and take action depending on the response SecureHeader header = new SecureHeader(Encoding.UTF8.GetString(response.Headers[SecureHeader.HEADER_NAME] as byte[])); if (header.Type == SecureMessageType.RespondMessage) { using (MemoryStream outputStream = new MemoryStream()) { // decrypt using (MemoryStream inputStream = new MemoryStream(response.Body)) { using (Aes aes = Aes.Create()) { aes.Key = _serverEncryptionKey; aes.IV = _serverNonce; using (CryptoStream decryptStream = new CryptoStream(inputStream, aes.CreateDecryptor(), CryptoStreamMode.Read)) { decryptStream.CopyTo(outputStream); } } } response.Body = outputStream.ToArray(); return(response); } } else if (header.Type == SecureMessageType.Error) { // deserialize SecureErrorMsg errorMsg = null; try { errorMsg = response.AsProtoBuf <SecureErrorMsg>(); } catch (Exception ex) { throw new InvalidDataException("The secure service sent an invalid error respsonse", ex); } throw new SecurityException($"{errorMsg.Message} ({errorMsg.Code})"); } else { throw new InvalidDataException($"The secure service sent an invalid response ({header.Type})"); } }