Skip to content

Commit

Permalink
Adds SharedAccessSignature to repo with fix for vulnerability (Azure#…
Browse files Browse the repository at this point in the history
…4943) (Azure#4967)

This is a cherry-pick of Azure#4943

This adds a subset of the code from https://github.com/Azure/azure-iot-sdk-csharp for SharedAccessSignature directly into our repo. This will also need to be cherry-picked into 1.1 and 1.2.

Todo:
- [x] Create UTs (UT=unit test) around this code, as there seem to be some tests that use parts of SharedAccessSignature but nothing completely dedicated
- [x] See if more code can be trimmed out that is not being used, just removed things without any references for first pass
- [x] if only using one supported .net then can maybe remove some other code here
  • Loading branch information
nyanzebra committed May 10, 2021
1 parent a95cce3 commit 4fea6e7
Show file tree
Hide file tree
Showing 12 changed files with 649 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Amqp
using Microsoft.Azure.Amqp;
using Microsoft.Azure.Amqp.Framing;
using Microsoft.Azure.Devices.Common;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Util;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy
{
using System;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Util;

class TokenHelper
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Authenticators
{
using System;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Cloud;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Authenticators
{
using System;
using System.Net;
using Microsoft.Azure.Devices.Common.Data;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Device;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Authenticators
{
using System;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
using Microsoft.Azure.Devices.Edge.Util;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Http.Middleware
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Device;
using Microsoft.Azure.Devices.Edge.Hub.Core.Identity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Test
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Common.Security;
using Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Authenticators;
using Microsoft.Azure.Devices.Edge.Hub.Core;
using Microsoft.Azure.Devices.Edge.Hub.Core.Device;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Util
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Text;

/// <summary>
/// A shared access signature, which can be used for authorization to an IoT Hub.
/// </summary>
public sealed class SharedAccessSignature
{
private readonly string encodedAudience;
private readonly string expiry;

private SharedAccessSignature(
string iotHubName,
DateTime expiresOn,
string expiry,
string keyName,
string signature,
string encodedAudience)
{
if (string.IsNullOrWhiteSpace(iotHubName))
{
throw new ArgumentNullException(nameof(iotHubName));
}

this.ExpiresOn = expiresOn;

if (this.IsExpired())
{
throw new UnauthorizedAccessException("The specified SAS token is expired");
}

this.IotHubName = iotHubName;
this.Signature = signature;
this.Audience = WebUtility.UrlDecode(encodedAudience);
this.encodedAudience = encodedAudience;
this.expiry = expiry;
this.KeyName = keyName ?? string.Empty;
}

/// <summary>
/// The IoT hub name.
/// </summary>
public string IotHubName { get; private set; }

/// <summary>
/// The date and time the SAS expires.
/// </summary>
public DateTime ExpiresOn { get; private set; }

/// <summary>
/// Name of the authorization rule.
/// </summary>
public string KeyName { get; private set; }

/// <summary>
/// The audience scope to which this signature applies.
/// </summary>
public string Audience { get; private set; }

/// <summary>
/// The value of the shared access signature.
/// </summary>
public string Signature { get; private set; }

/// <summary>
/// Parses a shared access signature string representation into a <see cref="SharedAccessSignature"/>./>
/// </summary>
/// <param name="iotHubName">The IoT Hub name.</param>
/// <param name="rawToken">The string representation of the SAS token to parse.</param>
/// <returns>The <see cref="SharedAccessSignature"/> instance that represents the passed in raw token.</returns>
public static SharedAccessSignature Parse(string iotHubName, string rawToken)
{
if (string.IsNullOrWhiteSpace(iotHubName))
{
throw new ArgumentNullException(nameof(iotHubName));
}

if (string.IsNullOrWhiteSpace(rawToken))
{
throw new ArgumentNullException(nameof(rawToken));
}

IDictionary<string, string> parsedFields = ExtractFieldValues(rawToken);

if (!parsedFields.TryGetValue(SharedAccessSignatureConstants.SignatureFieldName, out string signature))
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Missing field: {0}",
SharedAccessSignatureConstants.SignatureFieldName));
}

if (!parsedFields.TryGetValue(SharedAccessSignatureConstants.ExpiryFieldName, out string expiry))
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Missing field: {0}",
SharedAccessSignatureConstants.ExpiryFieldName));
}

// KeyName (skn) is optional.
parsedFields.TryGetValue(SharedAccessSignatureConstants.KeyNameFieldName, out string keyName);

if (!parsedFields.TryGetValue(SharedAccessSignatureConstants.AudienceFieldName, out string encodedAudience))
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Missing field: {0}",
SharedAccessSignatureConstants.AudienceFieldName));
}

return new SharedAccessSignature(
iotHubName,
SharedAccessSignatureConstants.EpochTime + TimeSpan.FromSeconds(double.Parse(expiry, CultureInfo.InvariantCulture)),
expiry,
keyName,
signature,
encodedAudience);
}

/// <summary>
/// Indicates if the token has expired.
/// </summary>
public bool IsExpired() => this.ExpiresOn + SharedAccessSignatureConstants.MaxClockSkew < DateTime.UtcNow;

/// <summary>
/// Authenticate against the IoT Hub using an authorization rule.
/// </summary>
/// <param name="sasAuthorizationRule">The properties that describe the keys to access the IotHub artifacts.</param>
public void Authenticate(SharedAccessSignatureAuthorizationRule sasAuthorizationRule)
{
if (sasAuthorizationRule == null)
{
throw new ArgumentNullException(nameof(sasAuthorizationRule), "The SAS Authorization Rule cannot be null.");
}

if (this.IsExpired())
{
throw new UnauthorizedAccessException("The specified SAS token has expired.");
}

if (sasAuthorizationRule.PrimaryKey != null)
{
string primaryKeyComputedSignature = ComputeSignature(Convert.FromBase64String(sasAuthorizationRule.PrimaryKey), this.encodedAudience, this.expiry);

if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(this.Signature), Encoding.UTF8.GetBytes(primaryKeyComputedSignature)))
{
return;
}
}

if (sasAuthorizationRule.SecondaryKey != null)
{
string secondaryKeyComputedSignature = ComputeSignature(Convert.FromBase64String(sasAuthorizationRule.SecondaryKey), this.encodedAudience, this.expiry);

if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(this.Signature), Encoding.UTF8.GetBytes(secondaryKeyComputedSignature)))
{
return;
}
}

throw new UnauthorizedAccessException("The specified SAS token has an invalid signature. It does not match either the primary or secondary key.");
}

/// <summary>
/// Compute the signature string using the SAS fields.
/// </summary>
/// <param name="key">Key used for computing the signature.</param>
/// <returns>The string representation of the signature.</returns>
public static string ComputeSignature(byte[] key, string encodedAudience, string expiry)
{
var fields = new List<string>
{
encodedAudience,
expiry,
};

using var hmac = new HMACSHA256(key);
string value = string.Join("\n", fields);
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(value)));
}

private static IDictionary<string, string> ExtractFieldValues(string sharedAccessSignature)
{
string[] lines = sharedAccessSignature.Split();

if (!string.Equals(lines[0].Trim(), SharedAccessSignatureConstants.SharedAccessSignature, StringComparison.Ordinal) || lines.Length != 2)
{
throw new FormatException("Malformed signature");
}

IDictionary<string, string> parsedFields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string[] fields = lines[1].Trim().Split(new string[] { SharedAccessSignatureConstants.PairSeparator }, StringSplitOptions.None);

foreach (string field in fields)
{
if (!string.IsNullOrEmpty(field))
{
string[] fieldParts = field.Split(new string[] { SharedAccessSignatureConstants.KeyValueSeparator }, StringSplitOptions.None);
if (string.Equals(fieldParts[0], SharedAccessSignatureConstants.AudienceFieldName, StringComparison.OrdinalIgnoreCase))
{
// We need to preserve the casing of the escape characters in the audience,
// so defer decoding the URL until later.
parsedFields.Add(fieldParts[0], fieldParts[1]);
}
else
{
parsedFields.Add(fieldParts[0], WebUtility.UrlDecode(fieldParts[1]));
}
}
}

return parsedFields;
}
}
}

0 comments on commit 4fea6e7

Please sign in to comment.