From 2dec45b0cd015c3f3a36f39ed38764539607daab Mon Sep 17 00:00:00 2001 From: George Krechar Date: Wed, 15 Aug 2018 14:16:07 -0700 Subject: [PATCH] Add X509EncryptedCredentials class If only a certificate is provided, key wrap encryption algorithm and data encryption algorithm will be set by default to RsaOaepKeyWrap and A128CBC-HS256, respectively. * Add new ctor to EncryptingCredentials to allow users to pass only a 'shared' symmetric key which will be used to encrypt data, but it will not be serialized to a SAML token. * Add internal const strings DefaultAsymmetricKeyWrapAlgorithm and DefaultSymmetricAlgorithm to indicate the default algorithms used for key wrap and data encryption * Add protected ctor to EncryptingCredentials to check if a certificate passed to X509EncryptedCredentials is null. Provides cleaner stack trace in case of an exception caused by a null cert. * Refactor EncryptingCredentials. Move null/empty checks to setters and provide clearer comments * Add tests for X509EncryiptingCredentials and EncryptingCredentials classes Resolves: #995 See also: #734 --- .../EncryptingCredentials.cs | 84 +++++++--- .../LogMessages.cs | 1 + .../SecurityAlgorithms.cs | 3 + .../X509EncryptingCredentials.cs | 77 +++++++++ test/Microsoft.IdentityModel.Tests/Default.cs | 5 + .../TestUtilities.cs | 8 + .../EncryptingCredentialsTests.cs | 146 ++++++++++++++++++ .../X509EncryptingCredentialsTests.cs | 119 ++++++++++++++ 8 files changed, 422 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs diff --git a/src/Microsoft.IdentityModel.Tokens/EncryptingCredentials.cs b/src/Microsoft.IdentityModel.Tokens/EncryptingCredentials.cs index 00cbbed15d..213aebb951 100644 --- a/src/Microsoft.IdentityModel.Tokens/EncryptingCredentials.cs +++ b/src/Microsoft.IdentityModel.Tokens/EncryptingCredentials.cs @@ -25,67 +25,109 @@ // //------------------------------------------------------------------------------ +using System; +using System.Security.Cryptography.X509Certificates; using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { /// - /// A wrapper class for properties that are used for token encryption. + /// A class for properties that are used for token encryption. /// public class EncryptingCredentials { + private string _alg; + private string _enc; + private SecurityKey _key; + + /// + /// Initializes a new instance of the class. + /// + /// . + /// A key wrap encryption algorithm to apply when encrypting a session key. + /// Data encryption algorithm to apply when encrypting plaintext. + /// if 'certificate' is null. + /// if 'alg' is null or empty. + /// if 'enc' is null or empty. + protected EncryptingCredentials(X509Certificate2 certificate, string alg, string enc) + { + if (certificate == null) + throw LogHelper.LogArgumentNullException(nameof(certificate)); + + Key = new X509SecurityKey(certificate); + Alg = alg; + Enc = enc; + } + /// /// Initializes a new instance of the class. /// /// - /// The key encryption algorithm to apply. - /// The encryption algorithm to apply. + /// A key wrap encryption algorithm to apply when encrypting a session key. + /// Data encryption algorithm to apply when encrypting plaintext. + /// if 'key' is null. + /// if 'alg' is null or empty. + /// if 'enc' is null or empty. public EncryptingCredentials(SecurityKey key, string alg, string enc) { - if (key == null) - throw LogHelper.LogArgumentNullException(nameof(key)); + Key = key; + Alg = alg; + Enc = enc; + } - if (string.IsNullOrWhiteSpace(alg)) - throw LogHelper.LogArgumentNullException(nameof(alg)); + /// + /// Initializes a new instance of the class. + /// + /// Used in scenarios when a key represents a 'shared' symmetric key. + /// For example, SAML 2.0 Assertion will be encrypted using a provided symmetric key + /// which won't be serialized to a SAML token. + /// + /// + /// Data encryption algorithm to apply when encrypting plaintext. + /// If the is not . + /// if 'enc' is null or empty. + public EncryptingCredentials(SecurityKey key, string enc) + { + Key = key; - if (string.IsNullOrWhiteSpace(enc)) - throw LogHelper.LogArgumentNullException(nameof(enc)); + if (key.GetType() != typeof(SymmetricSecurityKey)) + throw LogHelper.LogArgumentException("key", LogMessages.IDX10704); - Alg = alg; + //explicitly setting Alg to None + Alg = SecurityAlgorithms.None; Enc = enc; - Key = key; } /// - /// Gets the algorithm which used for token encryption. + /// Gets the key wrap encryption algorithm used for a session key encryption. /// public string Alg { - get; - private set; + get => _alg; + private set => _alg = string.IsNullOrEmpty(value) ? throw LogHelper.LogArgumentNullException("alg") : value; } /// - /// Gets the algorithm which used for token encryption. + /// Gets the data encryption algorithm used for plaintext encryption. /// public string Enc { - get; - private set; + get => _enc; + private set => _enc = string.IsNullOrEmpty(value) ? throw LogHelper.LogArgumentNullException("enc") : value; } /// - /// Users can override the default with this property. This factory will be used for creating encryition providers. + /// Users can override the default with this property. This factory will be used for creating encryption providers. /// public CryptoProviderFactory CryptoProviderFactory { get; set; } /// - /// Gets the which used for signature valdiation. + /// Gets the which used for signature validation. /// public SecurityKey Key { - get; - private set; + get => _key; + private set => _key = value ?? throw LogHelper.LogArgumentNullException("key"); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index a843eebbe2..7beb5108d2 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -178,6 +178,7 @@ internal static class LogMessages public const string IDX10701 = "IDX10701: Invalid JsonWebKey rsa keying material: '{0}'. Both modulus and exponent should be present"; public const string IDX10702 = "IDX10702: One or more private RSA key parts are null in the JsonWebKey: '{0}'"; public const string IDX10703 = "IDX10703: Cannot create symmetric security key. Key length is zero."; + public const string IDX10704 = "IDX10704: The SecurityKey provided is not a SymmetricSecurityKey."; // Json specific errors public const string IDX10801 = "IDX10801: Unable to create an RSA public key from the Exponent and Modulus found in the JsonWebKey: E: '{0}', N: '{1}'. See inner exception for additional details."; diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityAlgorithms.cs b/src/Microsoft.IdentityModel.Tokens/SecurityAlgorithms.cs index a9f50d116c..76eaa8fe84 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityAlgorithms.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityAlgorithms.cs @@ -113,6 +113,9 @@ public static class SecurityAlgorithms public const string Aes192CbcHmacSha384 = "A192CBC-HS384"; public const string Aes256CbcHmacSha512 = "A256CBC-HS512"; + internal const string DefaultAsymmetricKeyWrapAlgorithm = RsaOaepKeyWrap; + internal const string DefaultSymmetricAlgorithm = Aes128CbcHmacSha256; + #pragma warning restore 1591 } } diff --git a/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs b/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs new file mode 100644 index 0000000000..b62f126e8e --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/X509EncryptingCredentials.cs @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +using System; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// An designed for to construct an based on a x509 certificate. + /// + public class X509EncryptingCredentials : EncryptingCredentials + { + /// + /// Constructs an based on a x509 certificate. + /// + /// A + /// + /// algorithm will be used by default as a key wrap encryption algorithm + /// algorithm will be used by default as a data encryption algorithm + /// + /// if 'certificate' is null. + public X509EncryptingCredentials(X509Certificate2 certificate) + : this(certificate, SecurityAlgorithms.DefaultAsymmetricKeyWrapAlgorithm, SecurityAlgorithms.DefaultSymmetricAlgorithm) + { + + } + + /// + /// Constructs an based on the x509 certificate, key wrapping algorithm, and data encryption algorithm. + /// + /// A + /// A key wrapping algorithm + /// A data encryption algorithm + /// if 'certificate' is null. + /// if 'keyWrapEncryptionAlgorithm' is null or empty. + /// if 'dataEncryptionAlgorithm' is null or empty. + public X509EncryptingCredentials(X509Certificate2 certificate, string keyWrapEncryptionAlgorithm, string dataEncryptionAlgorithm) + : base(new X509SecurityKey(certificate), keyWrapEncryptionAlgorithm, dataEncryptionAlgorithm) + { + Certificate = certificate; + } + + /// + /// Gets a used by this instance. + /// + public X509Certificate2 Certificate + { + get; + private set; + } + } +} diff --git a/test/Microsoft.IdentityModel.Tests/Default.cs b/test/Microsoft.IdentityModel.Tests/Default.cs index fd817b58b4..61e2994ab1 100644 --- a/test/Microsoft.IdentityModel.Tests/Default.cs +++ b/test/Microsoft.IdentityModel.Tests/Default.cs @@ -117,6 +117,11 @@ public static SecurityKey AsymmetricSigningKeyPublic get => new X509SecurityKey(KeyingMaterial.DefaultCert_2048_Public); } + public static SecurityKey AsymmetricEncryptionKeyPublic + { + get => new X509SecurityKey(KeyingMaterial.DefaultCert_2048_Public); + } + #if !CrossVersionTokenValidation public static TokenValidationParameters AsymmetricEncryptSignTokenValidationParameters { diff --git a/test/Microsoft.IdentityModel.Tests/TestUtilities.cs b/test/Microsoft.IdentityModel.Tests/TestUtilities.cs index fe3469699b..dd340edc52 100644 --- a/test/Microsoft.IdentityModel.Tests/TestUtilities.cs +++ b/test/Microsoft.IdentityModel.Tests/TestUtilities.cs @@ -417,6 +417,14 @@ public static void CheckForArgumentNull(CompareContext context, string name, Exc context.Diffs.Add($"!(ex is ArgumentNullException) || !ex.Message.Contains({name})"); } + public static void CheckForArgumentException(CompareContext context, string name, Exception ex) + { + if (ex == null) + context.Diffs.Add($"expecting ArgumentException for parameter {name}. Exception is null."); + else if (!(ex is ArgumentException) || !ex.Message.Contains(name)) + context.Diffs.Add($"!(ex is ArgumentException) || !ex.Message.Contains({name})"); + } + public static byte[] HexToByteArray(string hexString) { byte[] bytes = new byte[hexString.Length / 2]; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs new file mode 100644 index 0000000000..b9bee3efc4 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/EncryptingCredentialsTests.cs @@ -0,0 +1,146 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.IdentityModel.Tests; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Tests +{ + public class EncryptingCredentialsTests + { + [Fact] + public void Constructors() + { + var context = TestUtilities.WriteHeader($"{this}", "Constructors", true); + + // public EncryptingCredentials(SecurityKey key, string alg, string enc) + try + { + new EncryptingCredentials(null, null, null); + TestUtilities.CheckForArgumentNull(context, "key", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "key", ex); + } + + try + { + new EncryptingCredentials(Default.SymmetricEncryptionKey128, null, SecurityAlgorithms.Aes128CbcHmacSha256); + TestUtilities.CheckForArgumentNull(context, "alg", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "alg", ex); + } + + try + { + new EncryptingCredentials(Default.AsymmetricEncryptionKeyPublic, SecurityAlgorithms.RsaOaepKeyWrap, null); + TestUtilities.CheckForArgumentNull(context, "enc", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "enc", ex); + } + + try + { + new EncryptingCredentials(Default.AsymmetricEncryptionKeyPublic, string.Empty, string.Empty); + TestUtilities.CheckForArgumentNull(context, "alg", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "alg", ex); + } + + var key = Default.AsymmetricEncryptionKeyPublic; + var encryptingCredentials = new EncryptingCredentials(key, SecurityAlgorithms.RsaOaepKeyWrap, SecurityAlgorithms.Aes192CbcHmacSha384); + if (!ReferenceEquals(encryptingCredentials.Key, key)) + context.Diffs.Add("!object.ReferenceEquals(encryptingCredentials.Key, key)"); + + if (!SecurityAlgorithms.RsaOaepKeyWrap.Equals(encryptingCredentials.Alg)) + context.Diffs.Add("!SecurityAlgorithms.RsaOaepKeyWrap.Equals(encryptingCredentials.Alg)"); + + if (!SecurityAlgorithms.Aes192CbcHmacSha384.Equals(encryptingCredentials.Enc)) + context.Diffs.Add("!SecurityAlgorithms.Aes192CbcHmacSha384.Equals(encryptingCredentials.Enc)"); + + // public EncryptingCredentials(SecurityKey key, string enc) + try + { + new EncryptingCredentials(null, null); + TestUtilities.CheckForArgumentNull(context, "key", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "key", ex); + } + + try + { + new EncryptingCredentials(Default.AsymmetricEncryptionKeyPublic, SecurityAlgorithms.Aes128CbcHmacSha256); + TestUtilities.CheckForArgumentException(context, "key", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentException(context, "key", ex); + } + + try + { + new EncryptingCredentials(Default.SymmetricEncryptionKey128, null); + TestUtilities.CheckForArgumentNull(context, "enc", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "enc", ex); + } + + try + { + new EncryptingCredentials(Default.SymmetricEncryptionKey128, String.Empty); + TestUtilities.CheckForArgumentNull(context, "enc", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "enc", ex); + } + + key = Default.SymmetricEncryptionKey128; + encryptingCredentials = new EncryptingCredentials(key, SecurityAlgorithms.Aes128CbcHmacSha256); + if (!ReferenceEquals(encryptingCredentials.Key, key)) + context.Diffs.Add("!ReferenceEquals(encryptingCredentials.Key, key)"); + + if (!SecurityAlgorithms.Aes128CbcHmacSha256.Equals(encryptingCredentials.Enc)) + context.Diffs.Add("!SecurityAlgorithms.Aes128CbcHmacSha256.Equals(encryptingCredentials.Enc)"); + + //check context for errors + TestUtilities.AssertFailIfErrors(context); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs new file mode 100644 index 0000000000..1ee5953d02 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/X509EncryptingCredentialsTests.cs @@ -0,0 +1,119 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.IdentityModel.Tests; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Tests +{ + public class X509EncryptingCredentialsTests + { + [Fact] + public void Constructors() + { + var context = TestUtilities.WriteHeader($"{this}", "Constructors", true); + + // public X509EncryptingCredentials(Certificate2 certificate) + try + { + new X509EncryptingCredentials(null); + TestUtilities.CheckForArgumentNull(context, "certificate", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "certificate", ex); + } + + var cert = Default.Certificate; + var encryptingCredentials = new X509EncryptingCredentials(cert); + + if (!ReferenceEquals(encryptingCredentials.Certificate, cert)) + context.Diffs.Add("!ReferenceEquals(encryptingCredentials.Certificate, cert)"); + + if (!SecurityAlgorithms.RsaOaepKeyWrap.Equals(encryptingCredentials.Alg)) + context.Diffs.Add("!SecurityAlgorithms.RsaOaepKeyWrap.Equals(encryptingCredentials.Alg)"); + + if (!SecurityAlgorithms.Aes128CbcHmacSha256.Equals(encryptingCredentials.Enc)) + context.Diffs.Add("!SecurityAlgorithms.Aes128CbcHmacSha256.Equals(encryptingCredentials.Enc)"); + + // public X509EncryptingCredentials(Certificate2 certificate, string alg, string enc) + try + { + new X509EncryptingCredentials(null, null, null); + TestUtilities.CheckForArgumentNull(context, "certificate", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "certificate", ex); + } + + try + { + new X509EncryptingCredentials(Default.Certificate, null, SecurityAlgorithms.Aes128CbcHmacSha256); + TestUtilities.CheckForArgumentNull(context, "alg", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "alg", ex); + } + + try + { + new X509EncryptingCredentials(Default.Certificate, SecurityAlgorithms.RsaOaepKeyWrap, null); + TestUtilities.CheckForArgumentNull(context, "enc", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "enc", ex); + } + + try + { + new X509EncryptingCredentials(Default.Certificate, string.Empty, string.Empty); + TestUtilities.CheckForArgumentNull(context, "alg", null); + } + catch (Exception ex) + { + TestUtilities.CheckForArgumentNull(context, "alg", ex); + } + + encryptingCredentials = new X509EncryptingCredentials(cert, SecurityAlgorithms.RsaOaepKeyWrap, SecurityAlgorithms.Aes192CbcHmacSha384); + if (!ReferenceEquals(encryptingCredentials.Certificate, cert)) + context.Diffs.Add("!ReferenceEquals(encryptingCredentials.Certificate, cert)"); + + if (!SecurityAlgorithms.RsaOaepKeyWrap.Equals(encryptingCredentials.Alg)) + context.Diffs.Add("!SecurityAlgorithms.RsaOaepKeyWrap.Equals(encryptingCredentials.Alg)"); + + if (!SecurityAlgorithms.Aes192CbcHmacSha384.Equals(encryptingCredentials.Enc)) + context.Diffs.Add("!SecurityAlgorithms.Aes192CbcHmacSha384.Equals(encryptingCredentials.Enc)"); + + //check context for errors + TestUtilities.AssertFailIfErrors(context); + } + } +} \ No newline at end of file