diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-04-28 19:56:30 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-04-28 20:01:00 -0700 |
commit | ea4325d172d8a2bc925ed362f1c35560b8c1f13e (patch) | |
tree | a04960f4cddd7f2223df83b03e89300c5051434c | |
parent | 01d8c73f818d30b20f86630d35d230b5168215d1 (diff) | |
download | DotNetOpenAuth-origin/jwt.zip DotNetOpenAuth-origin/jwt.tar.gz DotNetOpenAuth-origin/jwt.tar.bz2 |
Work toward support JWT access tokens.origin/jwt
13 files changed, 829 insertions, 7 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs index c9ceb81..8b92d64 100644 --- a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs +++ b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs @@ -139,20 +139,40 @@ namespace DotNetOpenAuth.Messaging { this.compressed = compressed; } + protected bool Encrypted { + get { return this.encrypted; } + } + + protected bool Compressed { + get { return this.compressed; } + } + + protected RSACryptoServiceProvider SigningKey { + get { return this.asymmetricSigning; } + } + + protected RSACryptoServiceProvider EncryptingKey { + get { return this.asymmetricEncrypting; } + } + + protected ICryptoKeyStore CryptoKeyStore { + get { return this.cryptoKeyStore; } + } + + protected string CryptoKeyBucket { + get { return this.cryptoKeyBucket; } + } + /// <summary> /// Serializes the specified message, including compression, encryption, signing, and nonce handling where applicable. /// </summary> /// <param name="message">The message to serialize. Must not be null.</param> /// <returns>A non-null, non-empty value.</returns> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] - public string Serialize(T message) { + public virtual string Serialize(T message) { Requires.NotNull(message, "message"); - message.UtcCreationDate = DateTime.UtcNow; - - if (this.decodeOnceOnly != null) { - message.Nonce = MessagingUtilities.GetNonCryptoRandomData(NonceLength); - } + this.BeforeSerialize(message); byte[] encoded = this.SerializeCore(message); @@ -189,6 +209,14 @@ namespace DotNetOpenAuth.Messaging { } } + protected virtual void BeforeSerialize(T message) { + message.UtcCreationDate = DateTime.UtcNow; + + if (this.decodeOnceOnly != null) { + message.Nonce = MessagingUtilities.GetNonCryptoRandomData(NonceLength); + } + } + /// <summary> /// Deserializes a <see cref="DataBag"/>, including decompression, decryption, signature and nonce validation where applicable. /// </summary> @@ -197,7 +225,7 @@ namespace DotNetOpenAuth.Messaging { /// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation.</param> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] - public void Deserialize(T message, IProtocolMessage containingMessage, string value, string messagePartName) { + public virtual void Deserialize(T message, IProtocolMessage containingMessage, string value, string messagePartName) { Requires.NotNull(message, "message"); Requires.NotNull(containingMessage, "containingMessage"); Requires.NotNullOrEmpty(value, "value"); @@ -236,6 +264,10 @@ namespace DotNetOpenAuth.Messaging { this.DeserializeCore(message, data); message.Signature = signature; // TODO: we don't really need this any more, do we? + this.AfterDeserialize(message, containingMessage); + } + + protected virtual void AfterDeserialize(T message, IProtocolMessage containingMessage) { if (this.maximumAge.HasValue) { // Has message verification code expired? DateTime expirationDate = message.UtcCreationDate + this.maximumAge.Value; diff --git a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj index 696d8a9..01bea78 100644 --- a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj +++ b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj @@ -20,6 +20,16 @@ <ItemGroup> <Compile Include="Configuration\OAuth2SectionGroup.cs" /> <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="OAuth2\Crypto\JwtRsaShaSigningAlgorithm.cs" /> + <Compile Include="OAuth2\Crypto\JweHeader.cs" /> + <Compile Include="OAuth2\Crypto\JweRsaEncryptionAlgorithm.cs" /> + <Compile Include="OAuth2\Crypto\JwsHeader.cs" /> + <Compile Include="OAuth2\Crypto\JwtEncryptionAlgorithm.cs" /> + <Compile Include="OAuth2\Crypto\JsonWebAlgorithms.cs" /> + <Compile Include="OAuth2\Crypto\JwtHeader.cs" /> + <Compile Include="OAuth2\Crypto\JwtHmacShaSigningAlgorithm.cs" /> + <Compile Include="OAuth2\Crypto\JwtMessageBase.cs" /> + <Compile Include="OAuth2\Crypto\JwtSigningAlgorithm.cs" /> <Compile Include="OAuth2\AccessToken.cs" /> <Compile Include="OAuth2\ChannelElements\AuthorizationDataBag.cs" /> <Compile Include="OAuth2\ChannelElements\ClientAuthenticationResult.cs" /> @@ -28,6 +38,7 @@ <Compile Include="OAuth2\ChannelElements\ScopeEncoder.cs" /> <Compile Include="OAuth2\ChannelElements\IAuthorizationDescription.cs" /> <Compile Include="OAuth2\ChannelElements\IAuthorizationCarryingRequest.cs" /> + <Compile Include="OAuth2\JsonWebTokenFormatter.cs" /> <Compile Include="OAuth2\Messages\AccessProtectedResourceRequest.cs" /> <Compile Include="OAuth2\Messages\UnauthorizedResponse.cs" /> <Compile Include="OAuth2\OAuthUtilities.cs" /> diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JsonWebAlgorithms.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JsonWebAlgorithms.cs new file mode 100644 index 0000000..fea18ba --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JsonWebAlgorithms.cs @@ -0,0 +1,141 @@ +namespace DotNetOpenAuth.OAuth2.Crypto { + internal static class JsonWebSignatureAlgorithms { + /// <summary> + /// HMAC using SHA-256 hash algorithm. + /// </summary> + internal const string HmacSha256 = "HS256"; + + /// <summary> + /// HMAC using SHA-384 hash algorithm. + /// </summary> + internal const string HmacSha384 = "HS384"; + + /// <summary> + /// HMAC using SHA-512 hash algorithm. + /// </summary> + internal const string HmacSha512 = "HS512"; + + /// <summary> + /// RSA using SHA-256 hash algorithm. + /// </summary> + internal const string RsaSha256 = "RS256"; + + /// <summary> + /// RSA using SHA-384 hash algorithm. + /// </summary> + internal const string RsaSha384 = "RS384"; + + /// <summary> + /// RSA using SHA-512 hash algorithm. + /// </summary> + internal const string RsaSha512 = "RS512"; + + /// <summary> + /// ECDSA using P-256 curve and SHA-256 hash algorithm. + /// </summary> + internal const string ECDsaSha256 = "ES256"; + + /// <summary> + /// ECDSA using P-384 curve and SHA-384 hash algorithm. + /// </summary> + internal const string ECDsaSha384 = "ES384"; + + /// <summary> + /// ECDSA using P-521 curve and SHA-512 hash algorithm. + /// </summary> + internal const string ECDsaSha512 = "ES512"; + + /// <summary> + /// No digital signature or HMAC value included. + /// </summary> + internal const string None = "none"; + } + + /// <summary> + /// The set of alg (algorithm) header parameter values that are defined by this + /// specification for use with JWE. These algorithms are used to encrypt the CEK, + /// which produces the JWE Encrypted Key. + /// </summary> + /// <remarks> + /// http://self-issued.info/docs/draft-ietf-jose-json-web-algorithms-01.html#EncAlgTable + /// </remarks> + internal static class JsonWebEncryptionAlgorithms { + /// <summary> + /// RSA using RSA-PKCS1-1.5 padding, as defined in RFC 3447 [RFC3447] + /// </summary> + internal const string RSA1_5 = "RSA1_5"; + + /// <summary> + /// RSA using Optimal Asymmetric Encryption Padding (OAEP), as defined in RFC 3447 [RFC3447] + /// </summary> + internal const string RSA_OAEP = "RSA-OAEP"; + + /// <summary> + /// Elliptic Curve Diffie-Hellman Ephemeral Static, as defined in RFC 6090 + /// [RFC6090], and using the Concat KDF, as defined in [NIST‑800‑56A], + /// where the Digest Method is SHA-256 and all OtherInfo parameters are + /// the empty bit string + /// </summary> + internal const string ECDH_ES = "ECDH-ES"; + + /// <summary> + /// Advanced Encryption Standard (AES) Key Wrap Algorithm using 128 bit keys, + /// as defined in RFC 3394 [RFC3394] + /// </summary> + internal const string A128KW = "A128KW"; + + /// <summary> + /// Advanced Encryption Standard (AES) Key Wrap Algorithm using 256 bit keys, + /// as defined in RFC 3394 [RFC3394] + /// </summary> + internal const string A256KW = "A256KW"; + + /// <summary> + /// Advanced Encryption Standard (AES) Key Wrap Algorithm using 512 bit keys, + /// as defined in RFC 3394 [RFC3394] + /// </summary> + internal const string A512KW = "A512KW"; + + /// <summary> + /// Advanced Encryption Standard (AES) using 128 bit keys in Galois/Counter + /// Mode, as defined in [FIPS‑197] and [NIST‑800‑38D] + /// </summary> + internal const string A128GCM = "A128GCM"; + + /// <summary> + /// Advanced Encryption Standard (AES) using 256 bit keys in Galois/Counter + /// Mode, as defined in [FIPS‑197] and [NIST‑800‑38D] + /// </summary> + internal const string A256GCM = "A256GCM"; + } + + /// <summary> + /// The set of enc (encryption method) header parameter values that are defined + /// by this specification for use with JWE. These algorithms are used to encrypt + /// the Plaintext, which produces the Ciphertext. + /// </summary> + /// <remarks> + /// http://self-issued.info/docs/draft-ietf-jose-json-web-algorithms-01.html#EncTable + /// </remarks> + internal static class JsonWebEncryptionMethods { + /// <summary> + /// Advanced Encryption Standard (AES) using 128 bit keys in Cipher Block Chaining mode, as defined in [FIPS‑197] and [NIST‑800‑38A] + /// </summary> + internal const string A128CBC = "A128CBC"; + + /// <summary> + /// Advanced Encryption Standard (AES) using 256 bit keys in Cipher Block Chaining mode, as defined in [FIPS‑197] and [NIST‑800‑38A] + /// </summary> + internal const string A256CBC = "A256CBC"; + + /// <summary> + /// Advanced Encryption Standard (AES) using 128 bit keys in Galois/Counter Mode, as defined in [FIPS‑197] and [NIST‑800‑38D] + /// </summary> + internal const string A128GCM = "A128GCM"; + + /// <summary> + /// Advanced Encryption Standard (AES) using 256 bit keys in Galois/Counter Mode, as defined in [FIPS‑197] and [NIST‑800‑38D] + /// </summary> + internal const string A256GCM = "A256GCM"; + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JweHeader.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JweHeader.cs new file mode 100644 index 0000000..2691da3 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JweHeader.cs @@ -0,0 +1,74 @@ +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal class JweHeader : JwtHeader { + private JweHeader() { + } + + internal JweHeader(string algorithm, string encryptionMethod) { + Requires.NotNullOrEmpty(algorithm, "algorithm"); + Requires.NotNullOrEmpty(encryptionMethod, "encryptionMethod"); + this.Algorithm = algorithm; + this.EncryptionMethod = encryptionMethod; + } + + /// <summary> + /// Gets or sets a value that identifies the cryptographic algorithm used to secure the JWS. + /// A list of defined alg values is presented in Section 3, Table 1 of the JSON Web Algorithms (JWA) [JWA] + /// specification. The processing of the alg header parameter requires that the value MUST be one that is + /// both supported and for which there exists a key for use with that algorithm associated with the party + /// that digitally signed or HMACed the content. The alg parameter value is case sensitive. + /// This header parameter is REQUIRED. + /// </summary> + [MessagePart("alg", IsRequired = true, AllowEmpty = false)] + internal string Algorithm { get; set; } + + /// <summary> + /// Gets or sets a value that identifies the symmetric encryption algorithm used to secure the Ciphertext. + /// A list of defined enc values is presented in Section 4, Table 3 of the JSON Web Algorithms (JWA) [JWA] + /// specification. The processing of the enc (encryption method) header parameter requires that the value + /// MUST be one that is supported. The enc value is case sensitive. This header parameter is REQUIRED. + /// </summary> + [MessagePart("enc", IsRequired = true, AllowEmpty = false)] + internal string EncryptionMethod { get; set; } + + /// <summary> + /// Gets or sets a value that identifies the cryptographic algorithm used to safeguard the integrity of the + /// Ciphertext and the parameters used to create it. The int parameter uses the same values as the JWS alg + /// parameter; a list of defined JWS alg values is presented in Section 3, Table 1 of the JSON Web Algorithms + /// (JWA) [JWA] specification. This header parameter is REQUIRED when an AEAD algorithm is not used to encrypt + /// the Plaintext and MUST NOT be present when an AEAD algorithm is used. + /// </summary> + [MessagePart("int")] + internal string IntegrityAlgorithm { get; set; } + + /// <summary> + /// Gets or sets a hint indicating which specific key owned by the signer should be used to validate the digital signature. + /// This allows signers to explicitly signal a change of key to recipients. The interpretation of the contents of the kid + /// parameter is unspecified. This header parameter is OPTIONAL. + /// </summary> + [MessagePart("kid")] + internal string KeyIdentity { get; set; } + + /// <summary> + /// Gets or sets the initialization Vector (iv) value for algorithms requiring it, represented as a base64url encoded string. + /// This header parameter is OPTIONAL. + /// </summary> + [MessagePart("iv", Encoder = typeof(Base64WebEncoder))] + internal byte[] IV { get; set; } + + /// <summary> + /// Gets or sets the compression algorithm (zip) applied to the Plaintext before encryption, if any. + /// This specification defines the value GZIP to refer to the encoding format produced by the file + /// compression program "gzip" (GNU zip) as described in [RFC1952]; this format is a Lempel-Ziv coding + /// (LZ77) with a 32 bit CRC. If no zip parameter is present, or its value is none, no compression is + /// applied to the Plaintext before encryption. The zip value is case sensitive. This header parameter is OPTIONAL. + /// </summary> + [MessagePart("zip")] + internal string CompressionAlgorithm { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JweRsaEncryptionAlgorithm.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JweRsaEncryptionAlgorithm.cs new file mode 100644 index 0000000..0d2159d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JweRsaEncryptionAlgorithm.cs @@ -0,0 +1,29 @@ +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + + internal class JweRsaEncryptionAlgorithm : JwtEncryptionAlgorithm { + private readonly RSACryptoServiceProvider recipientPublicKey; + + private readonly bool useOaepPadding; + + internal JweRsaEncryptionAlgorithm(RSACryptoServiceProvider recipientPublicKey, bool useOaepPadding = true) + : base(useOaepPadding ? JsonWebEncryptionAlgorithms.RSA_OAEP : JsonWebEncryptionAlgorithms.RSA1_5, JsonWebEncryptionMethods.A256CBC) { + Requires.NotNull(recipientPublicKey, "recipientPublicKey"); + this.recipientPublicKey = recipientPublicKey; + this.useOaepPadding = useOaepPadding; + } + + internal override void Encrypt(byte[] plainText, out byte[] cipherText, out byte[] integrityValue) { + cipherText = this.recipientPublicKey.Encrypt(plainText, this.useOaepPadding); + integrityValue = null; // RSA is an AEAD algorithm, so it doesn't need a separate integrity check. + } + + internal override byte[] Decrypt(byte[] cipherText, byte[] integrityValue) { + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwsHeader.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwsHeader.cs new file mode 100644 index 0000000..7274de5 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwsHeader.cs @@ -0,0 +1,36 @@ +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal class JwsHeader : JwtHeader { + private JwsHeader() { + } + + internal JwsHeader(string algorithm) { + Requires.NotNullOrEmpty(algorithm, "algorithm"); + this.Algorithm = algorithm; + } + + /// <summary> + /// Gets or sets a value that identifies the cryptographic algorithm used to secure the JWS. + /// A list of defined alg values is presented in Section 3, Table 1 of the JSON Web Algorithms (JWA) [JWA] + /// specification. The processing of the alg header parameter requires that the value MUST be one that is + /// both supported and for which there exists a key for use with that algorithm associated with the party + /// that digitally signed or HMACed the content. The alg parameter value is case sensitive. + /// This header parameter is REQUIRED. + /// </summary> + [MessagePart("alg", IsRequired = true, AllowEmpty = false)] + internal string Algorithm { get; set; } + + /// <summary> + /// Gets or sets a hint indicating which specific key owned by the signer should be used to validate the digital signature. + /// This allows signers to explicitly signal a change of key to recipients. The interpretation of the contents of the kid + /// parameter is unspecified. This header parameter is OPTIONAL. + /// </summary> + [MessagePart("kid")] + internal string KeyIdentity { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtEncryptionAlgorithm.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtEncryptionAlgorithm.cs new file mode 100644 index 0000000..b3ac78b --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtEncryptionAlgorithm.cs @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------- +// <copyright file="JwtEncryptionAlgorithm.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal abstract class JwtEncryptionAlgorithm : IDisposable { + protected JwtEncryptionAlgorithm(string algorithmName, string encryptionMethod) { + Requires.NotNullOrEmpty(algorithmName, "algorithmName"); + Requires.NotNullOrEmpty(encryptionMethod, "encryptionMethod"); + this.Header = new JweHeader(algorithmName, encryptionMethod); + } + + internal JweHeader Header { get; private set; } + + internal abstract void Encrypt(byte[] plainText, out byte[] cipherText, out byte[] integrityValue); + + internal abstract byte[] Decrypt(byte[] cipherText, byte[] integrityValue); + + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { + } + + protected void KeyDerivation(byte[] contentMasterKey, out byte[] contentEncryptionKey, out byte[] contentIntegrityKey) { + // Implementing this would be manual, or involve P/Invoke I think. + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375393(v=vs.85).aspx + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtHeader.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtHeader.cs new file mode 100644 index 0000000..1553946 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtHeader.cs @@ -0,0 +1,16 @@ +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal class JwtHeader : JwtMessageBase { + internal JwtHeader() { + this.Type = "JWT"; + } + + [MessagePart("typ")] + internal string Type { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtHmacShaSigningAlgorithm.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtHmacShaSigningAlgorithm.cs new file mode 100644 index 0000000..80d836c --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtHmacShaSigningAlgorithm.cs @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------- +// <copyright file="JwtHmacShaSigningAlgorithm.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal class JwtHmacShaSigningAlgorithm : JwtSigningAlgorithm { + private readonly HashAlgorithm algorithm; + + internal enum Algorithm { + HmacSha256, + HmacSha384, + HmacSha512, + } + + private JwtHmacShaSigningAlgorithm(string algorithmName, HMAC algorithm) + : base(algorithmName) { + Requires.NotNull(algorithm, "algorithm"); + this.algorithm = algorithm; + } + + internal static JwtSigningAlgorithm Create(Algorithm algorithm, string keyHandle, byte[] key) { + Requires.NotNull(key, "key"); + + string webAlgorithmName, cryptoName; + switch (algorithm) { + case Algorithm.HmacSha256: + cryptoName = "HMAC-SHA256"; + webAlgorithmName = JsonWebSignatureAlgorithms.HmacSha256; + break; + case Algorithm.HmacSha384: + cryptoName = "HMAC-SHA384"; + webAlgorithmName = JsonWebSignatureAlgorithms.HmacSha384; + break; + case Algorithm.HmacSha512: + cryptoName = "HMAC-SHA512"; + webAlgorithmName = JsonWebSignatureAlgorithms.HmacSha512; + break; + default: + Requires.InRange(false, "algorithm"); + throw Assumes.NotReachable(); + } + + HMAC hmac = null; + try { + hmac = HMAC.Create(cryptoName); + hmac.Key = key; + var result = new JwtHmacShaSigningAlgorithm(webAlgorithmName, hmac); + result.Header.KeyIdentity = keyHandle; + return result; + } catch { + if (hmac != null) { + hmac.Dispose(); + } + + throw; + } + } + + internal override byte[] Sign(byte[] securedInput) { + return algorithm.ComputeHash(securedInput); + } + + internal override bool Verify(byte[] securedInput, byte[] signature) { + return MessagingUtilities.AreEquivalentConstantTime(this.Sign(securedInput), signature); + } + + protected override void Dispose(bool disposing) { + if (disposing) { + this.algorithm.Dispose(); + } + + base.Dispose(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtMessageBase.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtMessageBase.cs new file mode 100644 index 0000000..04b9655 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtMessageBase.cs @@ -0,0 +1,26 @@ +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal class JwtMessageBase : IMessage { + private static readonly Version version = new Version(1, 0); + + private readonly Dictionary<string, string> extraData = new Dictionary<string, string>(); + + public Version Version { + get { return version; } + } + + public IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + public virtual void EnsureValidMessage() { + // The JWT spec mandates that any unexpected data in the JWT header or claims set cause a rejection. + ErrorUtilities.VerifyProtocol(this.ExtraData.Count == 0, "Unrecognized data in JWT access token with key '{0}'. Token rejected.", this.ExtraData.First().Key); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtRsaShaSigningAlgorithm.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtRsaShaSigningAlgorithm.cs new file mode 100644 index 0000000..48c1b60 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtRsaShaSigningAlgorithm.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------- +// <copyright file="JwtRsaShaSigningAlgorithm.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + internal class JwtRsaShaSigningAlgorithm : JwtSigningAlgorithm { + private readonly RSACryptoServiceProvider algorithm; + + private readonly HashAlgorithm hashAlgorithm; + + internal enum HashSize { + Sha256, + Sha384, + Sha512, + } + + internal JwtRsaShaSigningAlgorithm(string algorithmName, RSACryptoServiceProvider algorithm, HashAlgorithm hashAlgorithm) + : base(algorithmName) { + Requires.NotNull(algorithm, "algorithm"); + Requires.NotNull(hashAlgorithm, "hashAlgorithm"); + this.algorithm = algorithm; + this.hashAlgorithm = hashAlgorithm; + } + + internal static JwtRsaShaSigningAlgorithm Create(RSACryptoServiceProvider algorithm, HashSize hashSize) { + Requires.NotNull(algorithm, "algorithm"); + + string webAlgorithmName, cryptoName; + switch (hashSize) { + case HashSize.Sha256: + webAlgorithmName = JsonWebSignatureAlgorithms.RsaSha256; + cryptoName = "SHA256"; + break; + case HashSize.Sha384: + webAlgorithmName = JsonWebSignatureAlgorithms.RsaSha384; + cryptoName = "SHA384"; + break; + case HashSize.Sha512: + webAlgorithmName = JsonWebSignatureAlgorithms.RsaSha512; + cryptoName = "SHA512"; + break; + default: + Requires.InRange(false, "algorithm"); + throw Assumes.NotReachable(); + } + + HashAlgorithm hashAlgorithm = HashAlgorithm.Create(cryptoName); + try { + return new JwtRsaShaSigningAlgorithm(webAlgorithmName, algorithm, hashAlgorithm); + } catch { + hashAlgorithm.Dispose(); + throw; + } + } + + internal override byte[] Sign(byte[] securedInput) { + return algorithm.SignData(securedInput, this.hashAlgorithm); + } + + internal override bool Verify(byte[] securedInput, byte[] signature) { + return algorithm.VerifyData(securedInput, this.hashAlgorithm, signature); + } + + protected override void Dispose(bool disposing) { + if (disposing) { + // We only own the hash algorithm -- not the RSA algorithm. + this.hashAlgorithm.Dispose(); + } + + base.Dispose(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtSigningAlgorithm.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtSigningAlgorithm.cs new file mode 100644 index 0000000..116c1ca --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Crypto/JwtSigningAlgorithm.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="JwtSigningAlgorithm.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.Crypto { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + internal abstract class JwtSigningAlgorithm : IDisposable { + protected JwtSigningAlgorithm(string algorithmName) { + Requires.NotNullOrEmpty(algorithmName, "algorithmName"); + this.Header = new JwsHeader(algorithmName); + } + + internal JwsHeader Header { get; private set; } + + internal abstract byte[] Sign(byte[] securedInput); + + internal abstract bool Verify(byte[] securedInput, byte[] signature); + + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/JsonWebTokenFormatter.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/JsonWebTokenFormatter.cs new file mode 100644 index 0000000..8016533 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/JsonWebTokenFormatter.cs @@ -0,0 +1,217 @@ +namespace DotNetOpenAuth.OAuth2 { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Runtime.Serialization; + using System.Runtime.Serialization.Json; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth2.ChannelElements; + using DotNetOpenAuth.OAuth2.Crypto; + + internal class JsonWebTokenFormatter : DataBagFormatterBase<AccessToken> { + private static readonly Encoding JwtCharacterEncoding = Encoding.UTF8; + + private static readonly MessageDescriptionCollection messageDescriptions = new MessageDescriptionCollection(); + + private static readonly IMessageFactory jwtHeaderMessageFactory = ConstructJwtHeaderMessageFactory(); + + private static IMessageFactory ConstructJwtHeaderMessageFactory() { + var factory = new StandardMessageFactory(); + factory.AddMessageTypes(new MessageDescription[] { + messageDescriptions.Get(typeof(JwsHeader), new Version(1, 0)), + messageDescriptions.Get(typeof(JweHeader), new Version(1, 0)), + }); + + return factory; + } + + /// <summary> + /// Initializes a new instance of the <see cref="JsonWebTokenFormatter"/> class. + /// </summary> + /// <param name="signingKey">The crypto service provider with the asymmetric key to use for signing or verifying the token.</param> + /// <param name="encryptingKey">The crypto service provider with the asymmetric key to use for encrypting or decrypting the token.</param> + /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param> + /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param> + /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param> + protected internal JsonWebTokenFormatter(RSACryptoServiceProvider signingKey = null, RSACryptoServiceProvider encryptingKey = null, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) + : base(signingKey, encryptingKey, compressed, maximumAge, decodeOnceOnly) { + this.UseOaepPadding = true; + } + + /// <summary> + /// Initializes a new instance of the <see cref="JsonWebTokenFormatter"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param> + /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param> + /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param> + /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param> + /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param> + /// <param name="minimumAge">The minimum age.</param> + /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param> + /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param> + protected internal JsonWebTokenFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) + : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) { + Requires.True((cryptoKeyStore != null && !string.IsNullOrEmpty(bucket)) || (!signed && !encrypted), null); + this.UseOaepPadding = true; + } + + internal bool UseOaepPadding { get; set; } + + public override string Serialize(AccessToken message) { + this.BeforeSerialize(message); + + var claimsSet = new JwtClaims() { + IssuedAt = message.UtcIssued, + Principal = message.User, + Scope = message.Scope, + Id = message.Nonce, + }; + if (message.Lifetime.HasValue) { + claimsSet.NotAfter = message.UtcIssued + message.Lifetime.Value; + } + + byte[] encodedPayload = MessagingUtilities.SerializeAsJsonBytes(claimsSet, messageDescriptions); + + // First sign, then encrypt the payload, JWT style. + string jwt = this.CreateJsonWebEncryptionToken(JwtCharacterEncoding.GetBytes(this.CreateJsonWebSignatureToken(encodedPayload))); + return jwt; + } + + protected override byte[] SerializeCore(AccessToken message) { + throw new NotImplementedException(); + } + + public override void Deserialize(AccessToken message, IProtocolMessage containingMessage, string value, string messagePartName) { + string[] segments = value.Split(new [] {'.'}, 4); + ErrorUtilities.VerifyProtocol(segments.Length > 1, "Invalid JWT. No periods found."); + + string encodedHeader = segments[0]; + byte[] decodedHeader = MessagingUtilities.FromBase64WebSafeString(encodedHeader); + //jwtHeaderMessageFactory.GetNewRequestMessage(new MessageReceivingEndpoint(new Uri ("http://localhost/"), HttpDeliveryMethods.PostRequest), ); + var jwtHeader = new JwtHeader(); + MessagingUtilities.DeserializeFromJson(decodedHeader, jwtHeader, messageDescriptions, JwtCharacterEncoding); + // TODO: instantiate the appropriate JwtHeader type. + // TODO: Verify that ExtraData is empty. + + + + throw new NotImplementedException(); + + this.AfterDeserialize(message, containingMessage); + } + + protected override void DeserializeCore(AccessToken message, byte[] data) { + throw new NotImplementedException(); + } + + private static string SerializeSegment(IMessage message) { + return MessagingUtilities.ConvertToBase64WebSafeString(MessagingUtilities.SerializeAsJsonBytes(message, messageDescriptions, JwtCharacterEncoding)); + } + + private string CreateJsonWebSignatureToken(byte[] payload) { + Requires.NotNull(payload, "payload"); + Requires.ValidState(this.SigningKey != null, "An RSA signing key must be set first."); + + string encodedPayload = MessagingUtilities.ConvertToBase64WebSafeString(payload); + + KeyValuePair<string, CryptoKey> handleAndKey = this.CryptoKeyStore.GetKeys(this.CryptoKeyBucket).First(); + using (var algorithm = JwtRsaShaSigningAlgorithm.Create(this.SigningKey, JwtRsaShaSigningAlgorithm.HashSize.Sha256)) { + string encodedHeader = SerializeSegment(algorithm.Header); + + var builder = new StringBuilder(encodedHeader.Length + 1 + encodedPayload.Length); + builder.Append(encodedHeader); + builder.Append("."); + builder.Append(encodedPayload); + string securedInput = builder.ToString(); + + string encodedSignature = MessagingUtilities.ConvertToBase64WebSafeString(algorithm.Sign(JwtCharacterEncoding.GetBytes(securedInput))); + builder.Append("."); + builder.Append(encodedSignature); + + return builder.ToString(); + } + } + + private string CreateJsonWebEncryptionToken(byte[] payload) { + Requires.NotNull(payload, "payload"); + ErrorUtilities.VerifyInternal(this.Encrypted, "We shouldn't generate a JWE if we're not encrypting!"); + ErrorUtilities.VerifySupported(this.EncryptingKey != null, "Only asymmetric encryption is supported."); + + string encodedPayload = MessagingUtilities.ConvertToBase64WebSafeString(payload); + + var header = new JweHeader(this.UseOaepPadding ? JsonWebEncryptionAlgorithms.RSA_OAEP : JsonWebEncryptionAlgorithms.RSA1_5, JsonWebEncryptionMethods.A256CBC); + + var symmetricAlgorithm = SymmetricAlgorithm.Create("AES"); + symmetricAlgorithm.KeySize = 256; + symmetricAlgorithm.Mode = CipherMode.CBC; + header.IV = symmetricAlgorithm.IV; + + byte[] contentMasterKey = symmetricAlgorithm.Key; + byte[] encryptedKey = this.EncryptingKey.Encrypt(contentMasterKey, this.UseOaepPadding); + string encodedEncryptedKey = MessagingUtilities.ConvertToBase64WebSafeString(encryptedKey); + + byte[] plaintext = payload; + if (this.Compressed) { + header.CompressionAlgorithm = "GZIP"; + plaintext = MessagingUtilities.Compress(payload, MessagingUtilities.CompressionMethod.Gzip); + } + + var ciphertextStream = new MemoryStream(); + using (var encryptor = symmetricAlgorithm.CreateEncryptor()) { + using (var cryptoStream = new CryptoStream(ciphertextStream, encryptor, CryptoStreamMode.Write)) { + cryptoStream.Write(plaintext, 0, plaintext.Length); + cryptoStream.Flush(); + } + } + + string encodedCiphertext = MessagingUtilities.ConvertToBase64WebSafeString(ciphertextStream.ToArray()); + string encodedHeader = SerializeSegment(header); + + var builder = new StringBuilder(encodedHeader.Length + 1 + encodedPayload.Length); + builder.Append(encodedHeader); + builder.Append("."); + builder.Append(encodedEncryptedKey); + builder.Append("."); + builder.Append(encodedCiphertext); + builder.Append("."); + builder.Append(String.Empty); // the Encoded JWE Integrity Value is always empty because we use an AEAD encryption algorithm. + string securedInput = builder.ToString(); + + return builder.ToString(); + } + + private class JwtClaims : JwtMessageBase { + [MessagePart("exp", Encoder = typeof(TimestampEncoder))] + internal DateTime NotAfter { get; set; } + + [MessagePart("nbf", Encoder = typeof(TimestampEncoder))] + internal DateTime NotBefore { get; set; } + + [MessagePart("iat", Encoder = typeof(TimestampEncoder))] + internal DateTime IssuedAt { get; set; } + + [MessagePart("iss")] + internal string Issuer { get; set; } + + [MessagePart("aud")] + internal string Audience { get; set; } + + [MessagePart("prn")] + internal string Principal { get; set; } + + [MessagePart("jti")] + internal byte[] Id { get; set; } + + [MessagePart("typ")] + internal string Type { get; set; } + + [MessagePart("scope", Encoder = typeof(ScopeEncoder))] + internal HashSet<string> Scope { get; set; } + } + } +} |