//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId {
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Messages;
///
/// An association that uses the HMAC-SHA family of algorithms for message signing.
///
[ContractVerification(true)]
internal class HmacShaAssociation : Association {
///
/// A list of HMAC-SHA algorithms in order of decreasing bit lengths.
///
private static HmacSha[] hmacShaAssociationTypes = CreateAssociationTypes();
///
/// The specific variety of HMAC-SHA this association is based on (whether it be HMAC-SHA1, HMAC-SHA256, etc.)
///
private HmacSha typeIdentity;
///
/// Initializes a new instance of the class.
///
/// The specific variety of HMAC-SHA this association is based on (whether it be HMAC-SHA1, HMAC-SHA256, etc.)
/// The association handle.
/// The association secret.
/// The time duration the association will be good for.
private HmacShaAssociation(HmacSha typeIdentity, string handle, byte[] secret, TimeSpan totalLifeLength)
: base(handle, secret, totalLifeLength, DateTime.UtcNow) {
Requires.NotNull(typeIdentity, "typeIdentity");
Requires.NotNullOrEmpty(handle, "handle");
Requires.NotNull(secret, "secret");
Requires.InRange(totalLifeLength > TimeSpan.Zero, "totalLifeLength");
Contract.Ensures(this.TotalLifeLength == totalLifeLength);
ErrorUtilities.VerifyProtocol(secret.Length == typeIdentity.SecretLength, OpenIdStrings.AssociationSecretAndTypeLengthMismatch, secret.Length, typeIdentity.GetAssociationType(Protocol.Default));
this.typeIdentity = typeIdentity;
}
///
/// Gets the length (in bits) of the hash this association creates when signing.
///
public override int HashBitLength {
get {
Protocol protocol = Protocol.Default;
return HmacShaAssociation.GetSecretLength(protocol, this.GetAssociationType(protocol)) * 8;
}
}
///
/// Creates an HMAC-SHA association.
///
/// The OpenID protocol version that the request for an association came in on.
/// The value of the openid.assoc_type parameter.
/// The association handle.
/// The association secret.
/// How long the association will be good for.
/// The newly created association.
public static HmacShaAssociation Create(Protocol protocol, string associationType, string handle, byte[] secret, TimeSpan totalLifeLength) {
Requires.NotNull(protocol, "protocol");
Requires.NotNullOrEmpty(associationType, "associationType");
Requires.NotNull(secret, "secret");
Contract.Ensures(Contract.Result() != null);
HmacSha match = hmacShaAssociationTypes.FirstOrDefault(sha => string.Equals(sha.GetAssociationType(protocol), associationType, StringComparison.Ordinal));
ErrorUtilities.VerifyProtocol(match != null, OpenIdStrings.NoAssociationTypeFoundByName, associationType);
return new HmacShaAssociation(match, handle, secret, totalLifeLength);
}
///
/// Creates an association with the specified handle, secret, and lifetime.
///
/// The handle.
/// The secret.
/// Total lifetime.
/// The newly created association.
public static HmacShaAssociation Create(string handle, byte[] secret, TimeSpan totalLifeLength) {
Requires.NotNullOrEmpty(handle, "handle");
Requires.NotNull(secret, "secret");
Contract.Ensures(Contract.Result() != null);
HmacSha shaType = hmacShaAssociationTypes.FirstOrDefault(sha => sha.SecretLength == secret.Length);
ErrorUtilities.VerifyProtocol(shaType != null, OpenIdStrings.NoAssociationTypeFoundByLength, secret.Length);
return new HmacShaAssociation(shaType, handle, secret, totalLifeLength);
}
///
/// Returns the length of the shared secret (in bytes).
///
/// The protocol version being used that will be used to lookup the text in
/// The value of the protocol argument specifying the type of association. For example: "HMAC-SHA1".
/// The length (in bytes) of the association secret.
/// Thrown if no association can be found by the given name.
public static int GetSecretLength(Protocol protocol, string associationType) {
HmacSha match = hmacShaAssociationTypes.FirstOrDefault(shaType => string.Equals(shaType.GetAssociationType(protocol), associationType, StringComparison.Ordinal));
ErrorUtilities.VerifyProtocol(match != null, OpenIdStrings.NoAssociationTypeFoundByName, associationType);
return match.SecretLength;
}
///
/// Looks for the first association type in a preferred-order list that is
/// likely to be supported given a specific OpenID version and the security settings,
/// and perhaps a matching Diffie-Hellman session type.
///
/// The OpenID version that dictates which associations are available.
/// A value indicating whether to consider higher strength security to be better. Use true for initial association requests from the Relying Party; use false from Providers when the Relying Party asks for an unrecognized association in order to pick a suggested alternative that is likely to be supported on both sides.
/// The set of requirements the selected association type must comply to.
/// Use true for HTTP associations, false for HTTPS associations.
/// The resulting association type's well known protocol name. (i.e. HMAC-SHA256)
/// The resulting session type's well known protocol name, if a matching one is available. (i.e. DH-SHA256)
///
/// True if a qualifying association could be found; false otherwise.
///
internal static bool TryFindBestAssociation(Protocol protocol, bool highSecurityIsBetter, SecuritySettings securityRequirements, bool requireMatchingDHSessionType, out string associationType, out string sessionType) {
Requires.NotNull(protocol, "protocol");
Requires.NotNull(securityRequirements, "securityRequirements");
associationType = null;
sessionType = null;
// We use AsEnumerable() to avoid VerificationException (http://stackoverflow.com/questions/478422/why-does-simple-array-and-linq-generate-verificationexception-operation-could-de)
IEnumerable preferredOrder = highSecurityIsBetter ?
hmacShaAssociationTypes.AsEnumerable() : hmacShaAssociationTypes.Reverse();
foreach (HmacSha sha in preferredOrder) {
int hashSizeInBits = sha.SecretLength * 8;
if (hashSizeInBits > securityRequirements.MaximumHashBitLength ||
hashSizeInBits < securityRequirements.MinimumHashBitLength) {
continue;
}
if (OpenIdUtilities.IsDiffieHellmanPresent) {
sessionType = DiffieHellmanUtilities.GetNameForSize(protocol, hashSizeInBits);
} else {
sessionType = requireMatchingDHSessionType ? null : protocol.Args.SessionType.NoEncryption;
}
if (requireMatchingDHSessionType && sessionType == null) {
continue;
}
associationType = sha.GetAssociationType(protocol);
if (associationType == null) {
continue;
}
return true;
}
return false;
}
///
/// Determines whether a named Diffie-Hellman session type and association type can be used together.
///
/// The protocol carrying the names of the session and association types.
/// The value of the openid.assoc_type parameter.
/// The value of the openid.session_type parameter.
///
/// true if the named association and session types are compatible; otherwise, false.
///
internal static bool IsDHSessionCompatible(Protocol protocol, string associationType, string sessionType) {
Requires.NotNull(protocol, "protocol");
Requires.NotNullOrEmpty(associationType, "associationType");
Requires.NotNull(sessionType, "sessionType");
// All association types can work when no DH session is used at all.
if (string.Equals(sessionType, protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal)) {
return true;
}
if (OpenIdUtilities.IsDiffieHellmanPresent) {
// When there _is_ a DH session, it must match in hash length with the association type.
int associationSecretLengthInBytes = GetSecretLength(protocol, associationType);
int sessionHashLengthInBytes = DiffieHellmanUtilities.Lookup(protocol, sessionType).HashSize / 8;
return associationSecretLengthInBytes == sessionHashLengthInBytes;
} else {
return false;
}
}
///
/// Gets the string to pass as the assoc_type value in the OpenID protocol.
///
/// The protocol version of the message that the assoc_type value will be included in.
///
/// The value that should be used for the openid.assoc_type parameter.
///
[Pure]
internal override string GetAssociationType(Protocol protocol) {
return this.typeIdentity.GetAssociationType(protocol);
}
///
/// Returns the specific hash algorithm used for message signing.
///
///
/// The hash algorithm used for message signing.
///
[Pure]
protected override HashAlgorithm CreateHasher() {
var result = this.typeIdentity.CreateHasher(SecretKey);
Contract.Assume(result != null);
return result;
}
///
/// Returns the value used to initialize the static field storing association types.
///
/// A non-null, non-empty array.
/// >
/// This is a method rather than being inlined to the field initializer to try to avoid
/// the CLR bug that crops up sometimes if we initialize arrays using object initializer syntax.
///
private static HmacSha[] CreateAssociationTypes() {
return new[] {
new HmacSha {
HmacAlgorithmName = HmacAlgorithms.HmacSha384,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA512,
BaseHashAlgorithm = SHA512.Create(),
},
new HmacSha {
HmacAlgorithmName = HmacAlgorithms.HmacSha384,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA384,
BaseHashAlgorithm = SHA384.Create(),
},
new HmacSha {
HmacAlgorithmName = HmacAlgorithms.HmacSha256,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA256,
BaseHashAlgorithm = SHA256.Create(),
},
new HmacSha {
HmacAlgorithmName = HmacAlgorithms.HmacSha1,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA1,
BaseHashAlgorithm = SHA1.Create(),
},
};
}
///
/// Provides information about some HMAC-SHA hashing algorithm that OpenID supports.
///
private class HmacSha {
///
/// Gets or sets the function that takes a particular OpenID version and returns the value of the openid.assoc_type parameter in that protocol.
///
internal Func GetAssociationType { get; set; }
///
/// Gets or sets the name of the HMAC-SHA algorithm. (e.g. "HMAC-SHA256")
///
internal string HmacAlgorithmName { get; set; }
///
/// Gets or sets the base hash algorithm.
///
internal HashAlgorithm BaseHashAlgorithm { get; set; }
///
/// Gets the size of the hash (in bytes).
///
internal int SecretLength { get { return this.BaseHashAlgorithm.HashSize / 8; } }
///
/// Creates the using a given shared secret for the mac.
///
/// The HMAC secret.
/// The algorithm.
internal HashAlgorithm CreateHasher(byte[] secret) {
return HmacAlgorithms.Create(this.HmacAlgorithmName, secret);
}
}
}
}