Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Create connectionIds using RNGCrypto #1606

Merged
merged 11 commits into from
Mar 16, 2018
Merged

Create connectionIds using RNGCrypto #1606

merged 11 commits into from
Mar 16, 2018

Conversation

BrennanConroy
Copy link
Member

@@ -71,6 +78,7 @@ public DefaultConnectionContext CreateConnection(PipeOptions transportPipeOption
var connectionTimer = SocketEventSource.Log.ConnectionStart(id);
var pair = DuplexPipe.CreateConnectionPair(transportPipeOptions, appPipeOptions);

// TODO: We can possibly delay creating connections until we get the first request and check the connectionId
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a todo? If we're not doing this I'm not sure what the point is?

@JamesNK
Copy link
Member

JamesNK commented Mar 15, 2018

The issue talks about using an HMAC to validate the ID, but the PR is using HMAC as the ID, and no validation. Issue change in offline discussion?

return Guid.NewGuid().ToString();
var id = Interlocked.Increment(ref _id);
// HMAC the id because we want a cryptographically random id, which GUID is not
var hashed = _hmacHash.ComputeHash(Encoding.UTF8.GetBytes(id.ToString()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this whole thing makes no sense and we should just use rngcryptoserviceprovider

Copy link
Contributor

@analogrelay analogrelay Mar 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if the only outcome is to generate a more-different random number, why not just do that? If the goal is to validate something, we need a different approach.

@BrennanConroy
Copy link
Member Author

Yeah, lets talk offline today


// This tcs exists so that multiple calls to DisposeAsync all wait asynchronously
// on the same task
private TaskCompletionSource<object> _disposeTcs = new TaskCompletionSource<object>();

public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application)
public DefaultConnectionContext(string id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of these changes. I think you should just set the pipes on the outside. The options shouldn't be part of this class at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about people using this layer having to do that themselves?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK to require that callers have to set the pipe themselves. User applications (Endpoints) will get a fully configured ConnectionContext, so we're the only consumers of this during initialization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep the other ctor to avoid changing all of the callers. We're just adding a new optional overload.

Span<byte> buffer = stackalloc byte[16];
_keyGen.GetBytes(buffer);
// Generate the id with RNGCrypto because we want a cryptographically random id, which GUID is not
return WebEncoders.Base64UrlEncode(buffer.ToArray());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert to hex, don't base64 encode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Y u hate base64? It looks so much cleaner, and no ugly == because it's the URL encode variant.

Plus, the hex encoding code looks gross (BitConverter.ToString(buffer.ToArray()).Replace("-", ""))

Base64URL: MTIzNDU2NzgxMjM0NTY3OA
Hex:       31323334353637383132333435363738

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So much cleaner, looks more encrypty (it's "Base64 Encrypted" after all ;))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fite me irl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we need to update the Base64 encoder to support taking a span and we need to update it to use the new base64 decoder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until then, lets remove the stackalloc since that just forces us to allocate more.

@analogrelay
Copy link
Contributor

To close the loop on the discussions, we decided that the impact of HMACing or encrypting the key was heavy and would only gain us the ability to remove the initial allocation of the entry in the connection list. Keeping track of the IDs we've issued (which we do now) is equally secure, but we can reduce allocations by avoiding allocating the full DefaultConnectionContext on the /negotiate endpoint, and only allocate it when the client returns for the poll/SSE/WebSocket/send endpoints.

Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's tidy up the pipe initialization.

Base64 4 lyfe.

return WebEncoders.Base64UrlEncode(buffer.ToArray());
#else
// 128 bit buffer / 8 bits per byte = 16 bytes
var buffer = new byte[16];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this buffer is so short-lived and limited in scope, it it worth using ArrayPool<byte>.Shared to rent it? Legit not sure exactly what the use cases are for that. @davidfowl ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I think this is stackalloc worthy but only if we update WebEncoders.Base64UrlEncode(buffer) to take a Span<byte>


// This tcs exists so that multiple calls to DisposeAsync all wait asynchronously
// on the same task
private TaskCompletionSource<object> _disposeTcs = new TaskCompletionSource<object>();

public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application)
public DefaultConnectionContext(string id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK to require that callers have to set the pipe themselves. User applications (Endpoints) will get a fully configured ConnectionContext, so we're the only consumers of this during initialization.

@@ -22,6 +24,8 @@ public class ConnectionManager
// TODO: Consider making this configurable? At least for testing?
private static readonly TimeSpan _heartbeatTickRate = TimeSpan.FromSeconds(1);

private static readonly RNGCryptoServiceProvider _keyGen = new RNGCryptoServiceProvider();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_keyGenerator

@@ -23,7 +24,6 @@ public void NewConnectionsHaveConnectionId()
Assert.Null(connection.TransportTask);
Assert.Null(connection.Cancellation);
Assert.NotEqual(default(DateTime), connection.LastSeenUtc);
Assert.NotNull(connection.Transport);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert.Null

@BrennanConroy BrennanConroy changed the title Create connectionIds using HMAC Create connectionIds using RNGCrypto Mar 16, 2018
@BrennanConroy BrennanConroy changed the base branch from dev to release/2.1 March 16, 2018 18:43

Assert.NotNull(connection.ConnectionId);
Assert.NotNull(connection.Transport);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this back

@@ -555,6 +558,15 @@ private DefaultConnectionContext CreateConnectionInternal(HttpSocketOptions opti
return null;
}

if (connection.Transport == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment here.


private DefaultConnectionContext CreateConnectionInternal(HttpSocketOptions options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not bring this back and just check the transport here internally?

Copy link
Member

@davidfowl davidfowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits and comments, otherwise good to merge IMO

@@ -22,16 +22,20 @@ public class DefaultConnectionContext : ConnectionContext,
IConnectionHeartbeatFeature,
ITransferFormatFeature
{
private List<(Action<object> handler, object state)> _heartbeatHandlers = new List<(Action<object> handler, object state)>();
private object _heartBeatLock = new object();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: _heartbeatLock (lower case b)

public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application)
/// <summary>
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
/// You'll need to add Transport and Application pipes on your own.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to add Transport and Application pipes on your own

The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually

@@ -94,8 +106,12 @@ public void RemoveConnection(string id)

private static string MakeNewConnectionId()
{
// TODO: We need to sign and encyrpt this
return Guid.NewGuid().ToString();
// TODO: Use Span when WebEncoders implements Span methods
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File a bug

var transportPipeOptions = new PipeOptions(pauseWriterThreshold: options.TransportMaxBufferSize, resumeWriterThreshold: options.TransportMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
var appPipeOptions = new PipeOptions(pauseWriterThreshold: options.ApplicationMaxBufferSize, resumeWriterThreshold: options.ApplicationMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
return _manager.CreateConnection(transportPipeOptions, appPipeOptions);
// If the connection doesn't have a pipe yet then create one, we lazily create the pipe to save on allocations until the connection actually connects
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

until the client actually connects

@BrennanConroy BrennanConroy merged commit 0e38ee3 into release/2.1 Mar 16, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants