1
//-----------------------------------------------------------------------
2 // <copyright file="HmacShaAssociation.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth
.OpenId
{
9 using System
.Collections
.Generic
;
10 using System
.Diagnostics
;
12 using System
.Security
.Cryptography
;
13 using DotNetOpenAuth
.Messaging
;
14 using DotNetOpenAuth
.OpenId
;
15 using DotNetOpenAuth
.OpenId
.Messages
;
16 using DotNetOpenAuth
.OpenId
.Provider
;
19 /// An association that uses the HMAC-SHA family of algorithms for message signing.
21 internal class HmacShaAssociation
: Association
{
23 /// The default lifetime of a shared association when no lifetime is given
24 /// for a specific association type.
26 private static readonly TimeSpan DefaultMaximumLifetime
= TimeSpan
.FromDays(14);
29 /// A list of HMAC-SHA algorithms in order of decreasing bit lengths.
31 private static HmacSha
[] hmacShaAssociationTypes
= {
33 CreateHasher
= secretKey
=> new HMACSHA512(secretKey
),
34 GetAssociationType
= protocol
=> protocol
.Args
.SignatureAlgorithm
.HMAC_SHA512
,
35 BaseHashAlgorithm
= new SHA512Managed(),
38 CreateHasher
= secretKey
=> new HMACSHA384(secretKey
),
39 GetAssociationType
= protocol
=> protocol
.Args
.SignatureAlgorithm
.HMAC_SHA384
,
40 BaseHashAlgorithm
= new SHA384Managed(),
43 CreateHasher
= secretKey
=> new HMACSHA256(secretKey
),
44 GetAssociationType
= protocol
=> protocol
.Args
.SignatureAlgorithm
.HMAC_SHA256
,
45 BaseHashAlgorithm
= new SHA256Managed(),
48 CreateHasher
= secretKey
=> new HMACSHA1(secretKey
),
49 GetAssociationType
= protocol
=> protocol
.Args
.SignatureAlgorithm
.HMAC_SHA1
,
50 BaseHashAlgorithm
= new SHA1Managed(),
55 /// The specific variety of HMAC-SHA this association is based on (whether it be HMAC-SHA1, HMAC-SHA256, etc.)
57 private HmacSha typeIdentity
;
60 /// Initializes a new instance of the <see cref="HmacShaAssociation"/> class.
62 /// <param name="typeIdentity">The specific variety of HMAC-SHA this association is based on (whether it be HMAC-SHA1, HMAC-SHA256, etc.)</param>
63 /// <param name="handle">The association handle.</param>
64 /// <param name="secret">The association secret.</param>
65 /// <param name="totalLifeLength">The time duration the association will be good for.</param>
66 private HmacShaAssociation(HmacSha typeIdentity
, string handle
, byte[] secret
, TimeSpan totalLifeLength
)
67 : base(handle
, secret
, totalLifeLength
, DateTime
.UtcNow
) {
68 ErrorUtilities
.VerifyArgumentNotNull(typeIdentity
, "typeIdentity");
69 ErrorUtilities
.VerifyNonZeroLength(handle
, "handle");
70 ErrorUtilities
.VerifyArgumentNotNull(secret
, "secret");
71 ErrorUtilities
.VerifyProtocol(secret
.Length
== typeIdentity
.SecretLength
, OpenIdStrings
.AssociationSecretAndTypeLengthMismatch
, secret
.Length
, typeIdentity
.GetAssociationType(Protocol
.Default
));
73 this.typeIdentity
= typeIdentity
;
77 /// Creates an HMAC-SHA association.
79 /// <param name="protocol">The OpenID protocol version that the request for an association came in on.</param>
80 /// <param name="associationType">The value of the openid.assoc_type parameter.</param>
81 /// <param name="handle">The association handle.</param>
82 /// <param name="secret">The association secret.</param>
83 /// <param name="totalLifeLength">How long the association will be good for.</param>
84 /// <returns>The newly created association.</returns>
85 public static HmacShaAssociation
Create(Protocol protocol
, string associationType
, string handle
, byte[] secret
, TimeSpan totalLifeLength
) {
86 ErrorUtilities
.VerifyArgumentNotNull(protocol
, "protocol");
87 ErrorUtilities
.VerifyNonZeroLength(associationType
, "associationType");
88 ErrorUtilities
.VerifyArgumentNotNull(secret
, "secret");
90 HmacSha match
= hmacShaAssociationTypes
.FirstOrDefault(sha
=> String
.Equals(sha
.GetAssociationType(protocol
), associationType
, StringComparison
.Ordinal
));
91 ErrorUtilities
.VerifyProtocol(match
!= null, OpenIdStrings
.NoAssociationTypeFoundByName
, associationType
);
92 return new HmacShaAssociation(match
, handle
, secret
, totalLifeLength
);
96 /// Creates an association with the specified handle, secret, and lifetime.
98 /// <param name="handle">The handle.</param>
99 /// <param name="secret">The secret.</param>
100 /// <param name="totalLifeLength">Total lifetime.</param>
101 /// <returns>The newly created association.</returns>
102 public static HmacShaAssociation
Create(string handle
, byte[] secret
, TimeSpan totalLifeLength
) {
103 ErrorUtilities
.VerifyNonZeroLength(handle
, "handle");
104 ErrorUtilities
.VerifyArgumentNotNull(secret
, "secret");
106 HmacSha shaType
= hmacShaAssociationTypes
.FirstOrDefault(sha
=> sha
.SecretLength
== secret
.Length
);
107 ErrorUtilities
.VerifyProtocol(shaType
!= null, OpenIdStrings
.NoAssociationTypeFoundByLength
, secret
.Length
);
108 return new HmacShaAssociation(shaType
, handle
, secret
, totalLifeLength
);
112 /// Returns the length of the shared secret (in bytes).
114 /// <param name="protocol">The protocol version being used that will be used to lookup the text in <paramref name="associationType"/></param>
115 /// <param name="associationType">The value of the protocol argument specifying the type of association. For example: "HMAC-SHA1".</param>
116 /// <returns>The length (in bytes) of the association secret.</returns>
117 /// <exception cref="ProtocolException">Thrown if no association can be found by the given name.</exception>
118 public static int GetSecretLength(Protocol protocol
, string associationType
) {
119 HmacSha match
= hmacShaAssociationTypes
.FirstOrDefault(shaType
=> String
.Equals(shaType
.GetAssociationType(protocol
), associationType
, StringComparison
.Ordinal
));
120 ErrorUtilities
.VerifyProtocol(match
!= null, OpenIdStrings
.NoAssociationTypeFoundByName
, associationType
);
121 return match
.SecretLength
;
125 /// Creates a new association of a given type.
127 /// <param name="protocol">The protocol.</param>
128 /// <param name="associationType">Type of the association.</param>
129 /// <param name="associationUse">A value indicating whether the new association will be used privately by the Provider for "dumb mode" authentication
130 /// or shared with the Relying Party for "smart mode" authentication.</param>
131 /// <param name="securitySettings">The security settings of the Provider.</param>
132 /// <returns>The newly created association.</returns>
134 /// The new association is NOT automatically put into an association store. This must be done by the caller.
136 internal static HmacShaAssociation
Create(Protocol protocol
, string associationType
, AssociationRelyingPartyType associationUse
, ProviderSecuritySettings securitySettings
) {
137 ErrorUtilities
.VerifyArgumentNotNull(protocol
, "protocol");
138 ErrorUtilities
.VerifyNonZeroLength(associationType
, "associationType");
139 ErrorUtilities
.VerifyArgumentNotNull(securitySettings
, "securitySettings");
141 // Generate the handle. It must be unique, so we use a time element and a random data element to generate it.
142 string uniq
= MessagingUtilities
.GetCryptoRandomDataAsBase64(4);
143 string handle
= "{" + associationType + "}{" + DateTime.UtcNow.Ticks + "}{" + uniq + "}";
145 // Generate the secret that will be used for signing
146 int secretLength
= GetSecretLength(protocol
, associationType
);
147 byte[] secret
= MessagingUtilities
.GetCryptoRandomData(secretLength
);
150 if (associationUse
== AssociationRelyingPartyType
.Smart
) {
151 if (!securitySettings
.AssociationLifetimes
.TryGetValue(associationType
, out lifetime
)) {
152 lifetime
= DefaultMaximumLifetime
;
155 lifetime
= DumbSecretLifetime
;
158 return Create(protocol
, associationType
, handle
, secret
, lifetime
);
162 /// Looks for the first association type in a preferred-order list that is
163 /// likely to be supported given a specific OpenID version and the security settings,
164 /// and perhaps a matching Diffie-Hellman session type.
166 /// <param name="protocol">The OpenID version that dictates which associations are available.</param>
167 /// <param name="highSecurityIsBetter">A value indicating whether to consider higher strength security to be better. Use <c>true</c> for initial association requests from the Relying Party; use <c>false</c> 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.</param>
168 /// <param name="securityRequirements">The set of requirements the selected association type must comply to.</param>
169 /// <param name="requireMatchingDHSessionType">Use <c>true</c> for HTTP associations, <c>false</c> for HTTPS associations.</param>
170 /// <param name="associationType">The resulting association type's well known protocol name. (i.e. HMAC-SHA256)</param>
171 /// <param name="sessionType">The resulting session type's well known protocol name, if a matching one is available. (i.e. DH-SHA256)</param>
173 /// True if a qualifying association could be found; false otherwise.
175 internal static bool TryFindBestAssociation(Protocol protocol
, bool highSecurityIsBetter
, SecuritySettings securityRequirements
, bool requireMatchingDHSessionType
, out string associationType
, out string sessionType
) {
176 ErrorUtilities
.VerifyArgumentNotNull(protocol
, "protocol");
177 ErrorUtilities
.VerifyArgumentNotNull(securityRequirements
, "securityRequirements");
179 associationType
= null;
182 // We use AsEnumerable() to avoid VerificationException (http://stackoverflow.com/questions/478422/why-does-simple-array-and-linq-generate-verificationexception-operation-could-de)
183 IEnumerable
<HmacSha
> preferredOrder
= highSecurityIsBetter
?
184 hmacShaAssociationTypes
.AsEnumerable() : hmacShaAssociationTypes
.Reverse();
186 foreach (HmacSha sha
in preferredOrder
) {
187 int hashSizeInBits
= sha
.SecretLength
* 8;
188 if (hashSizeInBits
> securityRequirements
.MaximumHashBitLength
||
189 hashSizeInBits
< securityRequirements
.MinimumHashBitLength
) {
192 sessionType
= DiffieHellmanUtilities
.GetNameForSize(protocol
, hashSizeInBits
);
193 if (requireMatchingDHSessionType
&& sessionType
== null) {
196 associationType
= sha
.GetAssociationType(protocol
);
197 if (associationType
== null) {
208 /// Determines whether a named Diffie-Hellman session type and association type can be used together.
210 /// <param name="protocol">The protocol carrying the names of the session and association types.</param>
211 /// <param name="associationType">The value of the openid.assoc_type parameter.</param>
212 /// <param name="sessionType">The value of the openid.session_type parameter.</param>
214 /// <c>true</c> if the named association and session types are compatible; otherwise, <c>false</c>.
216 internal static bool IsDHSessionCompatible(Protocol protocol
, string associationType
, string sessionType
) {
217 ErrorUtilities
.VerifyArgumentNotNull(protocol
, "protocol");
218 ErrorUtilities
.VerifyNonZeroLength(associationType
, "associationType");
219 ErrorUtilities
.VerifyArgumentNotNull(sessionType
, "sessionType");
221 // All association types can work when no DH session is used at all.
222 if (string.Equals(sessionType
, protocol
.Args
.SessionType
.NoEncryption
, StringComparison
.Ordinal
)) {
226 // When there _is_ a DH session, it must match in hash length with the association type.
227 int associationSecretLengthInBytes
= GetSecretLength(protocol
, associationType
);
228 int sessionHashLengthInBytes
= DiffieHellmanUtilities
.Lookup(protocol
, sessionType
).HashSize
/ 8;
229 return associationSecretLengthInBytes
== sessionHashLengthInBytes
;
233 /// Gets the string to pass as the assoc_type value in the OpenID protocol.
235 /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param>
237 /// The value that should be used for the openid.assoc_type parameter.
239 internal override string GetAssociationType(Protocol protocol
) {
240 return this.typeIdentity
.GetAssociationType(protocol
);
244 /// Returns the specific hash algorithm used for message signing.
247 /// The hash algorithm used for message signing.
249 protected override HashAlgorithm
CreateHasher() {
250 return this.typeIdentity
.CreateHasher(SecretKey
);
254 /// Provides information about some HMAC-SHA hashing algorithm that OpenID supports.
256 private class HmacSha
{
258 /// Gets or sets the function that takes a particular OpenID version and returns the value of the openid.assoc_type parameter in that protocol.
260 internal Func
<Protocol
, string> GetAssociationType { get; set; }
263 /// Gets or sets a function that will create the <see cref="HashAlgorithm"/> using a given shared secret for the mac.
265 internal Func
<byte[], HashAlgorithm
> CreateHasher { get; set; }
268 /// Gets or sets the base hash algorithm.
270 internal HashAlgorithm BaseHashAlgorithm { get; set; }
273 /// Gets the size of the hash (in bytes).
275 internal int SecretLength { get { return this.BaseHashAlgorithm.HashSize / 8; }
}