1. Nick Sivo
  2. TwilioFluent

Commits

Nick Sivo  committed 28bc7cc

Improve signature verification to work around Twilio bug.

  • Participants
  • Parent commits 6c943b5
  • Branches default

Comments (0)

Files changed (5)

File TwilioFluent.Mvc3/TwilioAuthorizeAttribute.cs

View file
  • Ignore whitespace
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
 
-namespace TwilioFluent.Mvc3
-{
-	using System;
-	using System.Web.Mvc;
-	using System.Diagnostics;
+namespace TwilioFluent.Mvc3 {
+    using System;
+    using System.Web.Mvc;
 
-	/// <summary>
-	/// Validates signatures on incoming Twilio requests.
-	/// </summary>
-	public class TwilioAuthorizeAttribute : ActionFilterAttribute
-	{
-		/// <summary>
-		/// Gets or sets your application's Twilio auth token.
-		/// </summary>
-		public string AuthToken { get; set; }
+    /// <summary>
+    /// Validates signatures on incoming Twilio requests.
+    /// </summary>
+    public class TwilioAuthorizeAttribute : ActionFilterAttribute {
+        private TwilioRequestValidator _validator;
 
-		/// <summary>
-		/// Initializes a new instance of the <see cref="TwilioAuthorizeAttribute"/> class.
-		/// </summary>
-		/// <param name="authToken">Your application's Twilio auth token. Hint: Inherit from 
-		/// <see cref="TwilioAuthorizeAttribute"/> and hardcode it.</param>
-		public TwilioAuthorizeAttribute(string authToken)
-		{
-			AuthToken = authToken;
-		}
+        /// <summary>
+        /// Gets or sets your application's Twilio auth token.
+        /// </summary>
+        public string AuthToken { get; set; }
 
-		/// <summary>
-		/// Called by the ASP.NET MVC framework before the action method executes.
-		/// </summary>
-		/// <param name="filterContext">The filter context.</param>
-		public override void OnActionExecuting(ActionExecutingContext filterContext)
-		{
-			base.OnActionExecuting(filterContext);
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TwilioAuthorizeAttribute"/> class.
+        /// </summary>
+        /// <param name="authToken">Your application's Twilio auth token. Hint: Inherit from 
+        /// <see cref="TwilioAuthorizeAttribute"/> and hardcode it.</param>
+        public TwilioAuthorizeAttribute(string authToken) {
+            AuthToken = authToken;
+        }
 
-			var validator = new TwilioRequestValidator(AuthToken);
-			var request = filterContext.HttpContext.Request;
-			if (!validator.Validate(request))
-				throw new UnauthorizedAccessException("Invalid request signature.");
-		}
-	}
+        /// <summary>
+        /// Called by the ASP.NET MVC framework before the action method executes.
+        /// </summary>
+        /// <param name="filterContext">The filter context.</param>
+        public override void OnActionExecuting(ActionExecutingContext filterContext) {
+            base.OnActionExecuting(filterContext);
+
+            // Not thread safe, but also not thread unsafe...
+            var validator = _validator ?? (_validator = new TwilioRequestValidator(AuthToken));
+            var request = filterContext.HttpContext.Request;
+            if (!validator.Validate(request))
+                throw new UnauthorizedAccessException("Invalid request signature.");
+        }
+    }
 }

File TwilioFluent.Mvc3/TwilioRequestValidator.cs

View file
  • Ignore whitespace
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
 
-namespace TwilioFluent.Mvc3
-{
-	using System;
-	using System.Collections.Generic;
-	using System.Web;
-	using TwilioFluent.TwiML;
+namespace TwilioFluent.Mvc3 {
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Web;
+    using TwilioFluent.TwiML;
 
-	/// <summary>
-	/// Provides Twilio request verification
-	/// </summary>
-	public class TwilioRequestValidator
-	{
-		/// <summary>
-		/// The name of the signature header added by Twilio.
-		/// </summary>
-		public const string TwilioSignatureHeader = "X-Twilio-Signature";
+    /// <summary>
+    /// Provides Twilio request verification
+    /// </summary>
+    public class TwilioRequestValidator {
+        /// <summary>
+        /// The name of the signature header added by Twilio.
+        /// </summary>
+        public const string TwilioSignatureHeader = "X-Twilio-Signature";
 
-		private TwilioSignatureValidator _validator;
+        private readonly TwilioSignatureGenerator _generator;
 
-		/// <summary>
-		/// Initializes a new instance of the <see cref="TwilioRequestValidator"/> class.
-		/// </summary>
-		/// <param name="authToken">Your application's Twilio auth token.</param>
-		public TwilioRequestValidator(string authToken)
-		{
-			_validator = new TwilioSignatureValidator(authToken);
-		}
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TwilioRequestValidator"/> class.
+        /// </summary>
+        /// <param name="authToken">Your application's Twilio auth token.</param>
+        public TwilioRequestValidator(string authToken) {
+            _generator = new TwilioSignatureGenerator(authToken);
+        }
 
-		/// <summary>
-		/// Validates the Twilio signature on the specified request.
-		/// Cannot detect replay attacks, so use of SSL + Client Authentication is recommended.
-		/// </summary>
-		/// <param name="request">The request.</param>
-		/// <returns></returns>
-		public bool Validate(HttpRequestBase request)
-		{
-			var signatureString = request.Headers[TwilioSignatureHeader];
-			var parameters = new List<Tuple<string, string>>();
+        /// <summary>
+        /// Validates the Twilio signature on the specified request.
+        /// Cannot detect replay attacks, so use of SSL + Client Authentication is recommended.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns></returns>
+        public bool Validate(HttpRequestBase request) {
+            var signatureString = request.Headers[TwilioSignatureHeader];
+            if (string.IsNullOrWhiteSpace(signatureString))
+                return false;
 
-			if (request.HttpMethod == "POST")
-			{
-				foreach (var key in request.Form.AllKeys)
-				{
-					parameters.Add(Tuple.Create(key, request.Form[key]));
-				}
-			}
+            var suppliedSignature = TwilioSignature.FromHeader(signatureString);
 
-			return _validator.Validate(signatureString, request.Url.ToString(), parameters.ToArray());
-		}
-	}
+            var parameters = new List<Tuple<string, string>>();
+            if (request.HttpMethod == "POST") {
+                parameters.AddRange(request.Form.AllKeys.Select(key => Tuple.Create(key, request.Form[key])));
+            }
+
+            // Twilio gets this wrong if the scheme is HTTPS and a custom port is used :(
+            // return suppliedSignature == _generator.Generate(request.Url, parameters.ToArray());
+
+            if (suppliedSignature == _generator.Generate(request.Url, parameters.ToArray())) {
+                return true;
+            }
+            
+            if (request.Url.Scheme == Uri.UriSchemeHttps && !request.Url.IsDefaultPort) {
+                var builder = new UriBuilder(request.Url) { Port = -1 };
+                return suppliedSignature == _generator.Generate(builder.Uri, parameters.ToArray());
+            }
+
+            return false;
+        }
+    }
 }

File TwilioFluent.TwiML/TwilioSignature.cs

View file
  • Ignore whitespace
+//   Copyright 2011 Nicholas Sivo
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+
+
+namespace TwilioFluent.TwiML {
+    using System;
+
+    /// <summary>
+    /// Represents a Twilio signature.
+    /// </summary>
+    public struct TwilioSignature : IEquatable<TwilioSignature> {
+        private const int SignatureSize = 20;   // SHA1
+        private readonly byte[] _signature;
+
+        private TwilioSignature(byte[] signature) {
+            _signature = signature;
+        }
+
+        /// <summary>
+        /// Creates a TwilioSignature from the header passed by Twilio.
+        /// </summary>
+        /// <param name="headerValue">The signature header value passed by Twlio.</param>
+        public static TwilioSignature FromHeader(string headerValue) {
+            if (headerValue == null)
+                throw new ArgumentNullException("headerValue");
+            return new TwilioSignature(Convert.FromBase64String(headerValue));
+        }
+
+        /// <summary>
+        /// Creates a TwilioSignature from a byte array.
+        /// </summary>
+        /// <param name="signature">The signature.</param>
+        public static TwilioSignature FromHash(byte[] signature) {
+            if (signature == null)
+                throw new ArgumentNullException("signature");
+            if (signature.Length != SignatureSize)
+                throw new ArgumentOutOfRangeException("signature", string.Format("signature must be {0} bytes.", SignatureSize));
+            return new TwilioSignature(signature);
+        }
+
+        /// <summary>
+        /// Indicates whether the current object is equal to another object of the same type.
+        /// </summary>
+        /// <param name="other">An object to compare with this object.</param>
+        /// <returns>
+        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
+        /// </returns>
+        public bool Equals(TwilioSignature other) {
+            // Time invariant comparison (always the same number of operations)
+            // Prevents things like this: http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
+            var errors = 0;
+            for (var i = 0; i < _signature.Length; ++i) {
+                errors |= (_signature[i] ^ other._signature[i]);
+            }
+            return (errors == 0);
+        }
+
+        /// <summary>
+        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
+        /// </summary>
+        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
+        /// <returns>
+        ///   <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
+        /// </returns>
+        public override bool Equals(object obj) {
+            return obj != null
+                && obj is TwilioSignature
+                && Equals((TwilioSignature)obj);
+        }
+
+        /// <summary>
+        /// Implements the operator ==.
+        /// </summary>
+        public static bool operator ==(TwilioSignature a, TwilioSignature b) {
+            return a.Equals(b);
+        }
+
+        /// <summary>
+        /// Implements the operator !=.
+        /// </summary>
+        public static bool operator !=(TwilioSignature a, TwilioSignature b) {
+            return !a.Equals(b);
+        }
+
+        /// <summary>
+        /// Returns a hash code for this instance.
+        /// </summary>
+        /// <returns>
+        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
+        /// </returns>
+        public override int GetHashCode() {
+            var code = 0;
+            for (var i = 0; i < _signature.Length; ++i)
+                code ^= BitConverter.ToInt32(_signature, i);
+            return code;
+        }
+
+        /// <summary>
+        /// Returns a <see cref="System.String"/> that represents this instance.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="System.String"/> that represents this instance.
+        /// </returns>
+        public override string ToString() {
+            return Convert.ToBase64String(_signature);
+        }
+    }
+}

File TwilioFluent.TwiML/TwilioSignatureGenerator.cs

View file
  • Ignore whitespace
+//   Copyright 2011 Nicholas Sivo
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+
+namespace TwilioFluent.TwiML {
+    using System;
+    using System.Linq;
+    using System.Security.Cryptography;
+    using System.Text;
+
+    /// <summary>
+    /// Generates Twilio request signatures for verification.
+    /// </summary>
+    public class TwilioSignatureGenerator {
+        private readonly byte[] _secret;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TwilioSignatureGenerator"/> class.
+        /// </summary>
+        /// <param name="authToken">Your Twilio auth token.</param>
+        public TwilioSignatureGenerator(string authToken) {
+            if (authToken == null)
+                throw new ArgumentNullException("authToken");
+            _secret = Convert.FromBase64String(authToken);
+        }
+
+        /// <summary>
+        /// Generates a signature for specified Uri and POST parameters.
+        /// </summary>
+        /// <param name="fullUri">The full request Uri.</param>
+        /// <param name="parameters">The HTTP POST parameters.</param>
+        public TwilioSignature Generate(Uri fullUri, params Tuple<string, string>[] parameters) {
+            if (fullUri == null)
+                throw new ArgumentNullException("fullUri");
+
+            var temp = new StringBuilder();
+            temp.Append(fullUri.ToString());
+
+            foreach (var param in parameters.OrderBy(p => p.Item1, StringComparer.Ordinal)) {
+                temp.Append(param.Item1);
+                temp.Append(param.Item2);
+            }
+
+            using (var hmac = new HMACSHA1(_secret)) {
+                var fullString = temp.ToString();
+                var fullBytes = Encoding.ASCII.GetBytes(fullString);
+                var hash = hmac.ComputeHash(fullBytes);
+                return TwilioSignature.FromHash(hash);
+            }
+        }
+    }
+}

File TwilioFluent.TwiML/TwilioSignatureValidator.cs

  • Ignore whitespace
-//   Copyright 2011 Nicholas Sivo
-//
-//   Licensed under the Apache License, Version 2.0 (the "License");
-//   you may not use this file except in compliance with the License.
-//   You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-//   Unless required by applicable law or agreed to in writing, software
-//   distributed under the License is distributed on an "AS IS" BASIS,
-//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-//   See the License for the specific language governing permissions and
-//   limitations under the License.
-
-namespace TwilioFluent.TwiML
-{
-	using System;
-	using System.Linq;
-	using System.Security.Cryptography;
-	using System.Text;
-
-	/// <summary>
-	/// Provides Twilio request signature validation
-	/// </summary>
-	public class TwilioSignatureValidator
-	{
-		private byte[] _secret;
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="TwilioSignatureValidator"/> class.
-		/// </summary>
-		/// <param name="authToken">Your application's Twilio auth token.</param>
-		public TwilioSignatureValidator(string authToken)
-		{
-			_secret = Encoding.ASCII.GetBytes(authToken);
-		}
-
-		/// <summary>
-		/// Validates the Twilio signature on the specified request.
-		/// Cannot detect replay attacks, so use of SSL + Client Authentication is recommended.
-		/// </summary>
-		public bool Validate(string signatureString, string rawUrl, params Tuple<string, string>[] formParameters)
-		{
-			if (string.IsNullOrWhiteSpace(signatureString))
-				return false;
-			if (string.IsNullOrWhiteSpace(rawUrl))
-				return false;
-
-			var signature = Convert.FromBase64String(signatureString);
-
-			StringBuilder temp = new StringBuilder();
-			temp.Append(rawUrl);
-
-			foreach (var param in formParameters.OrderBy(p => p.Item1, StringComparer.Ordinal))
-			{
-				temp.Append(param.Item1);
-				temp.Append(param.Item2);
-			}
-
-			using (var hmac = new HMACSHA1(_secret))
-			{
-				if (signature.Length != (hmac.HashSize / 8))
-					return false;
-
-				var hash = hmac.ComputeHash(Encoding.ASCII.GetBytes(temp.ToString()));
-
-				// Time invariant comparison (always the same number of operations)
-				// Prevents things like this: http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
-				int errors = 0;
-				for (int i = 0; i < hash.Length; ++i)
-				{
-					errors |= (hash[i] ^ signature[i]);
-				}
-				return errors == 0;
-			}
-		}
-	}
-}