//----------------------------------------------------------------------- // // 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); } } } }