Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to set user credentials from strings(s) #885

Merged
merged 9 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/NATS.Client/ConnectionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ public IConnection CreateConnection(string url, string jwt, string privateNkey,
opts.SetUserCredentials(jwt, privateNkey);
return CreateConnection(opts, reconnectOnConnect);
}

/// <summary>
/// Attempt to connect to the NATS server referenced by <paramref name="url"/>
/// with NATS 2.0 the user jwt and nkey seed credentials provided directly in the string.
/// </summary>
/// <param name="url"></param>
/// <param name="credentialsText">The text containing the "-----BEGIN NATS USER JWT-----" block
/// and the text containing the "-----BEGIN USER NKEY SEED-----" block</param>
/// <param name="reconnectOnConnect"></param>
/// <returns></returns>
public IConnection CreateConnectionWithCredentials(string url, string credentialsText, bool reconnectOnConnect = false)
{
Options opts = GetDefaultOptions(url);
opts.SetUserCredentialsFromString(credentialsText);
return CreateConnection(opts, reconnectOnConnect);
}

/// <summary>
/// Attempt to connect to the NATS server referenced by <paramref name="url"/>
/// with NATS 2.0 the user jwt and nkey seed credentials provided directly via strings.
/// </summary>
/// <remarks>
/// <para><paramref name="url"/>
/// Comma seperated arrays are also supported, e.g. <c>&quot;urlA, urlB&quot;</c>.</para>
/// </remarks>
/// <param name="url">A string containing the URL (or URLs) to the NATS Server. See the Remarks
/// section for more information.</param>
/// <param name="userJwtText">The text containing the "-----BEGIN NATS USER JWT-----" block</param>
/// <param name="nkeySeedText">The text containing the "-----BEGIN USER NKEY SEED-----" block or the seed begining with "SU".
/// May be the same as the jwt string if they are chained.</param>
/// <param name="reconnectOnConnect"></param>
/// <returns></returns>
public IConnection CreateConnectionWithCredentials(string url, string userJwtText, string nkeySeedText, bool reconnectOnConnect = false)
{
Options opts = GetDefaultOptions(url);
opts.SetUserCredentialsFromStrings(userJwtText, nkeySeedText);
return CreateConnection(opts, reconnectOnConnect);
}

/// <summary>
/// Retrieves the default set of client options.
Expand Down
69 changes: 16 additions & 53 deletions src/NATS.Client/DefaultUserJWTHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// limitations under the License.

using System.IO;
using System.Security;

namespace NATS.Client
{
Expand Down Expand Up @@ -56,33 +57,19 @@ public DefaultUserJWTHandler(string jwtFilePath, string credsFilePath)
/// <returns>The encoded JWT</returns>
public static string LoadUserFromFile(string path)
{
string text = null;
string line = null;
StringReader reader = null;
try
string text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text))
{
text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text)) throw new NATSException("Credentials file is empty");

reader = new StringReader(text);
for (line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN NATS USER JWT-----"))
{
return reader.ReadLine();
}
Nkeys.Wipe(line);
}
throw new NATSException("Credentials file does not contain a JWT");
throw new NATSException("Credentials file is empty");
}
finally
string user = JWTHandlerUtils.LoadUser(text);
if (user == null)
{
Nkeys.Wipe(text);
Nkeys.Wipe(line);
reader?.Dispose();
throw new NATSException("Credentials file does not contain a JWT");
}
return user;
}

/// <summary>
/// Generates a NATS Ed25519 keypair, used to sign server nonces, from a
/// private credentials file.
Expand All @@ -91,47 +78,23 @@ public static string LoadUserFromFile(string path)
/// <returns>A NATS Ed25519 KeyPair</returns>
public static NkeyPair LoadNkeyPairFromSeedFile(string path)
{
NkeyPair kp = null;
string text = null;
string line = null;
string seed = null;
StringReader reader = null;

try
{
text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text)) throw new NATSException("Credentials file is empty");

// if it's a nk file, it only has the nkey
if (text.StartsWith("SU"))
string text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text))
{
kp = Nkeys.FromSeed(text);
return kp;
throw new NATSException("Credentials file is empty");
}

// otherwise assume it's a creds file.
reader = new StringReader(text);
for (line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN USER NKEY SEED-----"))
{
seed = reader.ReadLine();
kp = Nkeys.FromSeed(seed);
Nkeys.Wipe(seed);
}
Nkeys.Wipe(line);
}

NkeyPair kp = JWTHandlerUtils.LoadNkeyPair(text);
if (kp == null)
{
throw new NATSException("Seed not found in credentials file.");
else
return kp;
}
return kp;
}
finally
{
Nkeys.Wipe(line);
Nkeys.Wipe(text);
Nkeys.Wipe(seed);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FYI, these were removed because the wipe tries to wipe a string which is not possible. The wipe method was recently changed to a no-op.

reader?.Dispose();
}
}
Expand Down
72 changes: 72 additions & 0 deletions src/NATS.Client/JWTHandlerUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2019-2024 The NATS Authors
// Licensed 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.

using System.IO;
using System.Security;

namespace NATS.Client
{
public class JWTHandlerUtils
{
public static string LoadUser(string text)
{
StringReader reader = null;
try
{
reader = new StringReader(text);
for (string line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN NATS USER JWT-----"))
{
return reader.ReadLine();
}
}

return null;
}
finally
{
reader?.Dispose();
}
}

public static NkeyPair LoadNkeyPair(string nkeySeed)
{
StringReader reader = null;
try
{
// if it's a nk file, it only has the nkey
if (nkeySeed.StartsWith("SU"))
{
return Nkeys.FromSeed(nkeySeed);
}

// otherwise assume it's a creds file.
reader = new StringReader(nkeySeed);
for (string line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN USER NKEY SEED-----"))
{
return Nkeys.FromSeed(reader.ReadLine());
}
}

return null;
}
finally
{
reader?.Dispose();
}
}
}
}
7 changes: 4 additions & 3 deletions src/NATS.Client/NKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,10 @@ public static void Wipe(ref byte[] src)
/// <param name="src">string to wipe</param>
public static void Wipe(string src)
{
// best effort to wipe.
if (src != null && src.Length > 0)
src.Remove(0);
// This code commented out b/c string.remove does not touch the original string.
// There is not way to really wipe the contents of a string.
// if (src != null && src.Length > 0)
// src.Remove(0);
}

internal static byte[] DecodeSeed(byte[] raw)
Expand Down
25 changes: 25 additions & 0 deletions src/NATS.Client/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,31 @@ public void SetUserCredentials(string credentialsPath, string privateKeyPath)
UserSignatureEventHandler = handler.DefaultUserSignatureHandler;
}

/// <summary>
/// Sets user credentials from text instead of a file using the NATS 2.0 security scheme.
/// </summary>
/// <param name="credentialsText">The text containing the "-----BEGIN NATS USER JWT-----" block
/// and the text containing the "-----BEGIN USER NKEY SEED-----" block</param>
public void SetUserCredentialsFromString(string credentialsText)
{
var handler = new StringUserJWTHandler(credentialsText, credentialsText);
UserJWTEventHandler = handler.DefaultUserJWTEventHandler;
UserSignatureEventHandler = handler.DefaultUserSignatureHandler;
}

/// <summary>
/// Sets user credentials from text instead of a file using the NATS 2.0 security scheme.
/// </summary>
/// <param name="userJwtText">The text containing the "-----BEGIN NATS USER JWT-----" block</param>
/// <param name="nkeySeedText">The text containing the "-----BEGIN USER NKEY SEED-----" block or the seed begining with "SU".
/// May be the same as the jwt string if they are chained.</param>
public void SetUserCredentialsFromStrings(string userJwtText, string nkeySeedText)
{
var handler = new StringUserJWTHandler(userJwtText, nkeySeedText);
UserJWTEventHandler = handler.DefaultUserJWTEventHandler;
UserSignatureEventHandler = handler.DefaultUserSignatureHandler;
}

/// <summary>
/// Sets user credentials using the NATS 2.0 security scheme.
/// </summary>
Expand Down
93 changes: 93 additions & 0 deletions src/NATS.Client/StringUserJWTHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2024 The NATS Authors
// Licensed 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.

using System.IO;
using System.Security;

namespace NATS.Client
{
/// <summary>
/// TODO
/// </summary>
public class StringUserJWTHandler
{
/// <summary>
/// Gets the JWT file.
/// </summary>
public string UserJwt { get; }

/// <summary>
/// Gets the credentials files.
/// </summary>
public string NkeySeed { get; }

/// <summary>
/// Creates a static user jwt handler.
/// </summary>
/// <param name="credentialsText">The text containing the "-----BEGIN NATS USER JWT-----" block
/// and the text containing the "-----BEGIN USER NKEY SEED-----" block</param>
public StringUserJWTHandler(string credentialsText) : this(credentialsText, credentialsText) {}

/// <summary>
/// Creates a static user jwt handler.
/// </summary>
/// <param name="userJwt">The text containing the "-----BEGIN NATS USER JWT-----" block</param>
/// <param name="nkeySeed">The text containing the "-----BEGIN USER NKEY SEED-----" block or the seed begining with "SU".
/// May be the same as the jwt string if they are chained.</param>
public StringUserJWTHandler(string userJwt, string nkeySeed)
{
UserJwt = JWTHandlerUtils.LoadUser(userJwt);
if (UserJwt == null)
{
throw new NATSException("Credentials do not contain a JWT");
}

if (JWTHandlerUtils.LoadNkeyPair(nkeySeed) == null)
{
throw new NATSException("Seed not found.");
}
NkeySeed = nkeySeed;
}

/// <summary>
/// The default User JWT Event Handler.
/// </summary>
/// <param name="sender">Usually the connection.</param>
/// <param name="args">Arguments</param>
public void DefaultUserJWTEventHandler(object sender, UserJWTEventArgs args)
{
args.JWT = UserJwt;
}

/// <summary>
/// Utility method to signs the UserSignatureEventArgs server nonce from
/// a private credentials file.
/// </summary>
/// <param name="args">Arguments</param>
public void SignNonce(UserSignatureEventArgs args)
{
// you have to load this every time b/c signing actually wipes data
args.SignedNonce = JWTHandlerUtils.LoadNkeyPair(NkeySeed).Sign(args.ServerNonce);
}

/// <summary>
/// The default User Signature event handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void DefaultUserSignatureHandler(object sender, UserSignatureEventArgs args)
{
SignNonce(args);
}
}
}
9 changes: 9 additions & 0 deletions src/Tests/IntegrationTests/IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@
<None Update="config\tls_first.conf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\certs\test.creds.nkey-seed-block.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\certs\test.creds.nkey-seed-only.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\certs\test.creds.user-block.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Loading
Loading