From 58c66f12f61cd0e7372a1b1d54b4b03fff5a125c Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Tue, 9 Apr 2024 15:58:29 -0400 Subject: [PATCH] Ability to set user credentials from strings(s) (#885) --- src/NATS.Client/ConnectionFactory.cs | 38 ++++++++ src/NATS.Client/DefaultUserJWTHandler.cs | 69 ++++---------- src/NATS.Client/JWTHandlerUtils.cs | 72 ++++++++++++++ src/NATS.Client/NKeys.cs | 7 +- src/NATS.Client/Options.cs | 25 +++++ src/NATS.Client/StringUserJWTHandler.cs | 93 +++++++++++++++++++ .../IntegrationTests/IntegrationTests.csproj | 9 ++ src/Tests/IntegrationTests/TestConnection.cs | 36 +++++-- .../certs/test.creds.nkey-seed-block.txt | 7 ++ .../certs/test.creds.nkey-seed-only.txt | 1 + .../config/certs/test.creds.user-block.txt | 3 + 11 files changed, 298 insertions(+), 62 deletions(-) create mode 100644 src/NATS.Client/JWTHandlerUtils.cs create mode 100644 src/NATS.Client/StringUserJWTHandler.cs create mode 100644 src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-block.txt create mode 100644 src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-only.txt create mode 100644 src/Tests/IntegrationTests/config/certs/test.creds.user-block.txt diff --git a/src/NATS.Client/ConnectionFactory.cs b/src/NATS.Client/ConnectionFactory.cs index 615256145..b4577cb38 100644 --- a/src/NATS.Client/ConnectionFactory.cs +++ b/src/NATS.Client/ConnectionFactory.cs @@ -104,6 +104,44 @@ public IConnection CreateConnection(string url, string jwt, string privateNkey, opts.SetUserCredentials(jwt, privateNkey); return CreateConnection(opts, reconnectOnConnect); } + + /// + /// Attempt to connect to the NATS server referenced by + /// with NATS 2.0 the user jwt and nkey seed credentials provided directly in the string. + /// + /// + /// The text containing the "-----BEGIN NATS USER JWT-----" block + /// and the text containing the "-----BEGIN USER NKEY SEED-----" block + /// + /// + public IConnection CreateConnectionWithCredentials(string url, string credentialsText, bool reconnectOnConnect = false) + { + Options opts = GetDefaultOptions(url); + opts.SetUserCredentialsFromString(credentialsText); + return CreateConnection(opts, reconnectOnConnect); + } + + /// + /// Attempt to connect to the NATS server referenced by + /// with NATS 2.0 the user jwt and nkey seed credentials provided directly via strings. + /// + /// + /// + /// Comma seperated arrays are also supported, e.g. "urlA, urlB". + /// + /// A string containing the URL (or URLs) to the NATS Server. See the Remarks + /// section for more information. + /// The text containing the "-----BEGIN NATS USER JWT-----" block + /// 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. + /// + /// + public IConnection CreateConnectionWithCredentials(string url, string userJwtText, string nkeySeedText, bool reconnectOnConnect = false) + { + Options opts = GetDefaultOptions(url); + opts.SetUserCredentialsFromStrings(userJwtText, nkeySeedText); + return CreateConnection(opts, reconnectOnConnect); + } /// /// Retrieves the default set of client options. diff --git a/src/NATS.Client/DefaultUserJWTHandler.cs b/src/NATS.Client/DefaultUserJWTHandler.cs index 7158d987d..f82465ab6 100644 --- a/src/NATS.Client/DefaultUserJWTHandler.cs +++ b/src/NATS.Client/DefaultUserJWTHandler.cs @@ -12,6 +12,7 @@ // limitations under the License. using System.IO; +using System.Security; namespace NATS.Client { @@ -56,33 +57,19 @@ public DefaultUserJWTHandler(string jwtFilePath, string credsFilePath) /// The encoded JWT 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; } - + /// /// Generates a NATS Ed25519 keypair, used to sign server nonces, from a /// private credentials file. @@ -91,47 +78,23 @@ public static string LoadUserFromFile(string path) /// A NATS Ed25519 KeyPair 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); reader?.Dispose(); } } diff --git a/src/NATS.Client/JWTHandlerUtils.cs b/src/NATS.Client/JWTHandlerUtils.cs new file mode 100644 index 000000000..fd4138197 --- /dev/null +++ b/src/NATS.Client/JWTHandlerUtils.cs @@ -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(); + } + } + } +} diff --git a/src/NATS.Client/NKeys.cs b/src/NATS.Client/NKeys.cs index f83906d7d..c12af6a8e 100644 --- a/src/NATS.Client/NKeys.cs +++ b/src/NATS.Client/NKeys.cs @@ -283,9 +283,10 @@ public static void Wipe(ref byte[] src) /// string to wipe 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) diff --git a/src/NATS.Client/Options.cs b/src/NATS.Client/Options.cs index ef3509bc8..bf4f782d5 100644 --- a/src/NATS.Client/Options.cs +++ b/src/NATS.Client/Options.cs @@ -173,6 +173,31 @@ public void SetUserCredentials(string credentialsPath, string privateKeyPath) UserSignatureEventHandler = handler.DefaultUserSignatureHandler; } + /// + /// Sets user credentials from text instead of a file using the NATS 2.0 security scheme. + /// + /// The text containing the "-----BEGIN NATS USER JWT-----" block + /// and the text containing the "-----BEGIN USER NKEY SEED-----" block + public void SetUserCredentialsFromString(string credentialsText) + { + var handler = new StringUserJWTHandler(credentialsText, credentialsText); + UserJWTEventHandler = handler.DefaultUserJWTEventHandler; + UserSignatureEventHandler = handler.DefaultUserSignatureHandler; + } + + /// + /// Sets user credentials from text instead of a file using the NATS 2.0 security scheme. + /// + /// The text containing the "-----BEGIN NATS USER JWT-----" block + /// 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. + public void SetUserCredentialsFromStrings(string userJwtText, string nkeySeedText) + { + var handler = new StringUserJWTHandler(userJwtText, nkeySeedText); + UserJWTEventHandler = handler.DefaultUserJWTEventHandler; + UserSignatureEventHandler = handler.DefaultUserSignatureHandler; + } + /// /// Sets user credentials using the NATS 2.0 security scheme. /// diff --git a/src/NATS.Client/StringUserJWTHandler.cs b/src/NATS.Client/StringUserJWTHandler.cs new file mode 100644 index 000000000..064d48d0b --- /dev/null +++ b/src/NATS.Client/StringUserJWTHandler.cs @@ -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 +{ + /// + /// TODO + /// + public class StringUserJWTHandler + { + /// + /// Gets the JWT file. + /// + public string UserJwt { get; } + + /// + /// Gets the credentials files. + /// + public string NkeySeed { get; } + + /// + /// Creates a static user jwt handler. + /// + /// The text containing the "-----BEGIN NATS USER JWT-----" block + /// and the text containing the "-----BEGIN USER NKEY SEED-----" block + public StringUserJWTHandler(string credentialsText) : this(credentialsText, credentialsText) {} + + /// + /// Creates a static user jwt handler. + /// + /// The text containing the "-----BEGIN NATS USER JWT-----" block + /// 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. + 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; + } + + /// + /// The default User JWT Event Handler. + /// + /// Usually the connection. + /// Arguments + public void DefaultUserJWTEventHandler(object sender, UserJWTEventArgs args) + { + args.JWT = UserJwt; + } + + /// + /// Utility method to signs the UserSignatureEventArgs server nonce from + /// a private credentials file. + /// + /// Arguments + 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); + } + + /// + /// The default User Signature event handler. + /// + /// + /// + public void DefaultUserSignatureHandler(object sender, UserSignatureEventArgs args) + { + SignNonce(args); + } + } +} diff --git a/src/Tests/IntegrationTests/IntegrationTests.csproj b/src/Tests/IntegrationTests/IntegrationTests.csproj index bab278193..487244868 100644 --- a/src/Tests/IntegrationTests/IntegrationTests.csproj +++ b/src/Tests/IntegrationTests/IntegrationTests.csproj @@ -103,6 +103,15 @@ Always + + Always + + + Always + + + Always + diff --git a/src/Tests/IntegrationTests/TestConnection.cs b/src/Tests/IntegrationTests/TestConnection.cs index 6dd0ed6a2..2321acb0d 100644 --- a/src/Tests/IntegrationTests/TestConnection.cs +++ b/src/Tests/IntegrationTests/TestConnection.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; using NATS.Client; @@ -736,15 +737,38 @@ public void TestInvalidNKey() [Fact] public void Test20Security() + { + var opts = Context.GetTestOptionsWithDefaultTimeout(Context.Server1.Port); + // opts.SetUserCredentials("./config/certs/test.creds"); + // _testCredsSecurity(opts); + + string path = Directory.GetCurrentDirectory(); + string credentialsText = File.ReadAllText("./config/certs/test.creds"); + string userJwt = File.ReadAllText("./config/certs/test.creds.user-block.txt"); + string nkeySeedBlock = File.ReadAllText("./config/certs/test.creds.nkey-seed-block.txt"); + string nkeySeedOnly = File.ReadAllText("./config/certs/test.creds.nkey-seed-only.txt"); + + opts = Context.GetTestOptionsWithDefaultTimeout(Context.Server1.Port); + opts.SetUserCredentialsFromString(credentialsText); + _testCredsSecurity(opts); + + opts = Context.GetTestOptionsWithDefaultTimeout(Context.Server1.Port); + opts.SetUserCredentialsFromStrings(userJwt, nkeySeedBlock); + _testCredsSecurity(opts); + + opts = Context.GetTestOptionsWithDefaultTimeout(Context.Server1.Port); + opts.SetUserCredentialsFromStrings(userJwt, nkeySeedOnly); + _testCredsSecurity(opts); + } + + private void _testCredsSecurity(Options opts) { AutoResetEvent ev = new AutoResetEvent(false); + opts.ReconnectedEventHandler += (obj, args) => { + ev.Set(); + }; using (var s1 = NATSServer.CreateWithConfig(Context.Server1.Port, "operator.conf")) { - var opts = Context.GetTestOptionsWithDefaultTimeout(Context.Server1.Port); - opts.ReconnectedEventHandler += (obj, args) => { - ev.Set(); - }; - opts.SetUserCredentials("./config/certs/test.creds"); using (Context.ConnectionFactory.CreateConnection(opts)) { s1.Shutdown(); @@ -753,7 +777,7 @@ public void Test20Security() using (NATSServer.CreateWithConfig(Context.Server1.Port, "operator.conf")) { // wait for reconnect. - Assert.True(ev.WaitOne(60000)); + Assert.True(ev.WaitOne(30000)); } } } diff --git a/src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-block.txt b/src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-block.txt new file mode 100644 index 000000000..647103af0 --- /dev/null +++ b/src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-block.txt @@ -0,0 +1,7 @@ +************************* IMPORTANT ************************* +NKEY Seed printed below can be used sign and prove identity. +NKEYs are sensitive and should be treated as secrets. + +-----BEGIN USER NKEY SEED----- +SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4 +------END USER NKEY SEED------ diff --git a/src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-only.txt b/src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-only.txt new file mode 100644 index 000000000..777a66f16 --- /dev/null +++ b/src/Tests/IntegrationTests/config/certs/test.creds.nkey-seed-only.txt @@ -0,0 +1 @@ +SUAIBDPBAUTWCWBKIO6XHQNINK5FWJW4OHLXC3HQ2KFE4PEJUA44CNHTC4 \ No newline at end of file diff --git a/src/Tests/IntegrationTests/config/certs/test.creds.user-block.txt b/src/Tests/IntegrationTests/config/certs/test.creds.user-block.txt new file mode 100644 index 000000000..b8fd49846 --- /dev/null +++ b/src/Tests/IntegrationTests/config/certs/test.creds.user-block.txt @@ -0,0 +1,3 @@ +-----BEGIN NATS USER JWT----- +eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJFU1VQS1NSNFhGR0pLN0FHUk5ZRjc0STVQNTZHMkFGWERYQ01CUUdHSklKUEVNUVhMSDJBIiwiaWF0IjoxNTQ0MjE3NzU3LCJpc3MiOiJBQ1pTV0JKNFNZSUxLN1FWREVMTzY0VlgzRUZXQjZDWENQTUVCVUtBMzZNSkpRUlBYR0VFUTJXSiIsInN1YiI6IlVBSDQyVUc2UFY1NTJQNVNXTFdUQlAzSDNTNUJIQVZDTzJJRUtFWFVBTkpYUjc1SjYzUlE1V002IiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.kCR9Erm9zzux4G6M-V2bp7wKMKgnSNqMBACX05nwePRWQa37aO_yObbhcJWFGYjo1Ix-oepOkoyVLxOJeuD8Bw +------END NATS USER JWT------