536cd135cc
Former-commit-id: 5624ac747d633e885131e8349322922b6a59baaa
354 lines
14 KiB
C#
354 lines
14 KiB
C#
//------------------------------------------------------------
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//------------------------------------------------------------
|
|
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
|
|
namespace System.IdentityModel
|
|
{
|
|
/// <summary>
|
|
/// Provides cookie integrity using <see cref="RSA"/> signature.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <see cref="RsaSignatureCookieTransform"/> adds an RSA MAC to
|
|
/// the cookie data. This provides integrity but not confidentiality. By
|
|
/// default the MAC uses SHA-256, but SHA-1 may be requested.
|
|
/// </para>
|
|
/// <para>
|
|
/// Cookies signed with this transform may be read
|
|
/// by any machine that shares the same RSA private key (generally
|
|
/// associated with an X509 certificate).
|
|
/// </para>
|
|
/// </remarks>
|
|
public class RsaSignatureCookieTransform : CookieTransform
|
|
{
|
|
RSA _signingKey;
|
|
List<RSA> _verificationKeys = new List<RSA>();
|
|
string _hashName = "SHA256";
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of <see cref="RsaSignatureCookieTransform"/>.
|
|
/// </summary>
|
|
/// <param name="key">The provided key will be used as the signing and verification key by default.</param>
|
|
/// <exception cref="ArgumentNullException">When the key is null.</exception>
|
|
public RsaSignatureCookieTransform(RSA key)
|
|
{
|
|
if (null == key)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("key");
|
|
}
|
|
_signingKey = key;
|
|
_verificationKeys.Add(_signingKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of <see cref="RsaSignatureCookieTransform"/>
|
|
/// </summary>
|
|
/// <param name="certificate">Certificate whose private key is used to sign and verify.</param>
|
|
/// <exception cref="ArgumentNullException">When certificate is null.</exception>
|
|
/// <exception cref="ArgumentException">When the certificate has no private key.</exception>
|
|
/// <exception cref="ArgumentException">When the certificate's key is not RSA.</exception>
|
|
public RsaSignatureCookieTransform(X509Certificate2 certificate)
|
|
{
|
|
if (null == certificate)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("certificate");
|
|
}
|
|
_signingKey = X509Util.EnsureAndGetPrivateRSAKey(certificate);
|
|
_verificationKeys.Add(_signingKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the name of the hash algorithm to use.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// SHA256 is the default algorithm. This may require a minimum platform of Windows Server 2003 and .NET 3.5 SP1.
|
|
/// If SHA256 is not supported, set HashName to "SHA1".
|
|
/// </remarks>
|
|
public string HashName
|
|
{
|
|
get { return _hashName; }
|
|
set
|
|
{
|
|
using (HashAlgorithm algorithm = CryptoHelper.CreateHashAlgorithm(value))
|
|
{
|
|
if (algorithm == null)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("value", SR.GetString(SR.ID6034, value));
|
|
}
|
|
_hashName = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of <see cref="RsaSignatureCookieTransform"/>.
|
|
/// The instance created by this constructor is not usable until the signing and verification keys are set.
|
|
/// </summary>
|
|
internal RsaSignatureCookieTransform()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the RSA key used for signing
|
|
/// </summary>
|
|
public virtual RSA SigningKey
|
|
{
|
|
get { return _signingKey; }
|
|
set
|
|
{
|
|
_signingKey = value;
|
|
_verificationKeys = new List<RSA>(new RSA[] { _signingKey });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the collection of keys used for signature verification.
|
|
/// By default, this property returns a list containing only the signing key.
|
|
/// </summary>
|
|
protected virtual ReadOnlyCollection<RSA> VerificationKeys
|
|
{
|
|
get
|
|
{
|
|
return _verificationKeys.AsReadOnly();
|
|
}
|
|
}
|
|
|
|
// Format:
|
|
// SignatureLength : 4-byte big-endian integer
|
|
// Signature : Octet stream, length is SignatureLength
|
|
// CookieValue : Octet stream, remainder of message
|
|
|
|
/// <summary>
|
|
/// Verifies the signature. All keys in the collection VerificationKeys will be attempted.
|
|
/// </summary>
|
|
/// <param name="encoded">Data previously returned from <see cref="Encode"/></param>
|
|
/// <returns>The originally signed data.</returns>
|
|
/// <exception cref="ArgumentNullException">The argument 'encoded' is null.</exception>
|
|
/// <exception cref="ArgumentException">The argument 'encoded' contains zero bytes.</exception>
|
|
/// <exception cref="FormatException">The data is in the wrong format.</exception>
|
|
/// <exception cref="CryptographicException">The signature is invalid.</exception>
|
|
/// <exception cref="NotSupportedException">The platform does not support the requested algorithm.</exception>
|
|
/// <exception cref="InvalidOperationException">There are no verification keys.</exception>
|
|
public override byte[] Decode(byte[] encoded)
|
|
{
|
|
if (null == encoded)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("encoded");
|
|
}
|
|
|
|
if (0 == encoded.Length)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("encoded", SR.GetString(SR.ID6045));
|
|
}
|
|
|
|
ReadOnlyCollection<RSA> verificationKeys = VerificationKeys;
|
|
|
|
if (0 == verificationKeys.Count)
|
|
{
|
|
throw DiagnosticUtility.ThrowHelperInvalidOperation(SR.GetString(SR.ID6036));
|
|
}
|
|
|
|
// Decode the message ...
|
|
int currentIndex = 0;
|
|
|
|
// SignatureLength : 4-byte big-endian integer
|
|
if (encoded.Length < sizeof(Int32))
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new FormatException(SR.GetString(SR.ID1012)));
|
|
}
|
|
Int32 signatureLength = BitConverter.ToInt32(encoded, currentIndex);
|
|
|
|
if (signatureLength < 0)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new FormatException(SR.GetString(SR.ID1005, signatureLength)));
|
|
}
|
|
|
|
if (signatureLength >= encoded.Length - sizeof(Int32))
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new FormatException(SR.GetString(SR.ID1013)));
|
|
}
|
|
currentIndex += sizeof(Int32);
|
|
|
|
// Signature : Octet stream, length is SignatureLength
|
|
byte[] signature = new byte[signatureLength];
|
|
Array.Copy(encoded, currentIndex, signature, 0, signature.Length);
|
|
currentIndex += signature.Length;
|
|
|
|
// CookieValue : Octet stream, remainder of message
|
|
byte[] cookieValue = new byte[encoded.Length - currentIndex];
|
|
Array.Copy(encoded, currentIndex, cookieValue, 0, cookieValue.Length);
|
|
|
|
bool verified = false;
|
|
try
|
|
{
|
|
// Verify the signature
|
|
using (HashAlgorithm hash = CryptoHelper.CreateHashAlgorithm(HashName))
|
|
{
|
|
hash.ComputeHash(cookieValue);
|
|
|
|
foreach (RSA rsa in verificationKeys)
|
|
{
|
|
AsymmetricSignatureDeformatter verifier = GetSignatureDeformatter(rsa);
|
|
if ((isSha256() && CryptoHelper.VerifySignatureForSha256(verifier, hash, signature)) ||
|
|
verifier.VerifySignature(hash, signature))
|
|
{
|
|
verified = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Not all algorithms are supported on all OS
|
|
catch (CryptographicException e)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new NotSupportedException(SR.GetString(SR.ID6035, HashName, verificationKeys[0].GetType().FullName), e));
|
|
}
|
|
|
|
if (!verified)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new CryptographicException(SR.GetString(SR.ID1014)));
|
|
}
|
|
|
|
return cookieValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Signs data.
|
|
/// </summary>
|
|
/// <param name="value">Data to be signed.</param>
|
|
/// <returns>Signed data.</returns>
|
|
/// <exception cref="ArgumentNullException">The argument 'value' is null.</exception>
|
|
/// <exception cref="ArgumentException">The argument 'value' contains zero bytes.</exception>
|
|
/// <exception cref="InvalidOperationException">The SigningKey is null.</exception>
|
|
/// <exception cref="NotSupportedException">The platform does not support the requested algorithm.</exception>
|
|
/// <exception cref="InvalidOperationException">The SigningKey is null, is not an RSACryptoServiceProvider, or does not contain a private key.</exception>
|
|
/// <remarks>The SigningKey must include the private key in order to sign.</remarks>
|
|
public override byte[] Encode(byte[] value)
|
|
{
|
|
if (null == value)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("value");
|
|
}
|
|
|
|
if (0 == value.Length)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("value", SR.GetString(SR.ID6044));
|
|
}
|
|
|
|
RSA signingKey = SigningKey;
|
|
if (null == signingKey)
|
|
{
|
|
throw DiagnosticUtility.ThrowHelperInvalidOperation(SR.GetString(SR.ID6042));
|
|
}
|
|
|
|
RSACryptoServiceProvider rsaCryptoServiceProvider = signingKey as RSACryptoServiceProvider;
|
|
if (rsaCryptoServiceProvider == null && LocalAppContextSwitches.DisableCngCertificates)
|
|
{
|
|
throw DiagnosticUtility.ThrowHelperInvalidOperation(SR.GetString(SR.ID6042));
|
|
}
|
|
|
|
if (rsaCryptoServiceProvider != null && rsaCryptoServiceProvider.PublicOnly)
|
|
{
|
|
throw DiagnosticUtility.ThrowHelperInvalidOperation(SR.GetString(SR.ID6046));
|
|
}
|
|
|
|
// Compute the signature
|
|
byte[] signature;
|
|
using (HashAlgorithm hash = CryptoHelper.CreateHashAlgorithm(HashName))
|
|
{
|
|
try
|
|
{
|
|
hash.ComputeHash(value);
|
|
AsymmetricSignatureFormatter signer = GetSignatureFormatter(signingKey);
|
|
|
|
if (isSha256())
|
|
{
|
|
signature = CryptoHelper.CreateSignatureForSha256(signer, hash);
|
|
}
|
|
else
|
|
{
|
|
signature = signer.CreateSignature(hash);
|
|
}
|
|
}
|
|
// Not all algorithms are supported on all OS
|
|
catch (CryptographicException e)
|
|
{
|
|
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new NotSupportedException(SR.GetString(SR.ID6035, HashName, signingKey.GetType().FullName), e));
|
|
}
|
|
}
|
|
|
|
// Get the signature length as a big-endian integer
|
|
byte[] signatureLength = BitConverter.GetBytes(signature.Length);
|
|
|
|
// Assemble the message ...
|
|
int currentIndex = 0;
|
|
byte[] message = new byte[signatureLength.Length + signature.Length + value.Length];
|
|
|
|
// SignatureLength : 4-byte big endian integer
|
|
Array.Copy(signatureLength, 0, message, currentIndex, signatureLength.Length);
|
|
currentIndex += signatureLength.Length;
|
|
|
|
// Signature : Octet stream, length is SignatureLength
|
|
Array.Copy(signature, 0, message, currentIndex, signature.Length);
|
|
currentIndex += signature.Length;
|
|
|
|
// CookieValue : Octet stream, remainder of message
|
|
Array.Copy(value, 0, message, currentIndex, value.Length);
|
|
|
|
return message;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default RSACryptoServiceProvider does not support signatures for SHA256. If this is desired, it's necessary to construct a new one.
|
|
/// </summary>
|
|
AsymmetricSignatureFormatter GetSignatureFormatter(RSA rsa)
|
|
{
|
|
RSACryptoServiceProvider rsaProvider = rsa as RSACryptoServiceProvider;
|
|
if (isSha256() && null != rsaProvider)
|
|
{
|
|
return CryptoHelper.GetSignatureFormatterForSha256(rsaProvider);
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// If it's SHA-1 or the RSA is not an RsaCSP, just create a formatter using the original RSA.
|
|
//
|
|
return new RSAPKCS1SignatureFormatter(rsa);
|
|
}
|
|
}
|
|
|
|
AsymmetricSignatureDeformatter GetSignatureDeformatter(RSA rsa)
|
|
{
|
|
RSACryptoServiceProvider rsaProvider = rsa as RSACryptoServiceProvider;
|
|
if (isSha256() && null != rsaProvider)
|
|
{
|
|
return CryptoHelper.GetSignatureDeFormatterForSha256(rsaProvider);
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// If it's SHA-1 or the RSA is not an RsaCSP, just create a deformatter using the original RSA.
|
|
//
|
|
return new RSAPKCS1SignatureDeformatter(rsa);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the hash algorithm is set to SHA256, false otherwise.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
bool isSha256()
|
|
{
|
|
return (StringComparer.OrdinalIgnoreCase.Equals(HashName, "SHA256")
|
|
|| StringComparer.OrdinalIgnoreCase.Equals(HashName, "SHA-256")
|
|
|| StringComparer.OrdinalIgnoreCase.Equals(HashName, "System.Security.Cryptography.SHA256"));
|
|
}
|
|
}
|
|
}
|