Skip to content

Commit 6ca3775

Browse files
TonewallTony Choi
andauthored
Use auth to decrypt signature and use container name for audience for… (#9348)
* Use auth to decrypt signature and use container name for audience for cv1 * Adding unit tests * Unit test resetting container name * clean up * Encrypt and decrypt with signature test --------- Co-authored-by: Tony Choi <[email protected]>
1 parent 84a27c7 commit 6ca3775

File tree

5 files changed

+121
-15
lines changed

5 files changed

+121
-15
lines changed

src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,22 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
7373

7474
private static string[] GetValidAudiences()
7575
{
76-
if (SystemEnvironment.Instance.IsPlaceholderModeEnabled() &&
77-
SystemEnvironment.Instance.IsFlexConsumptionSku())
76+
if (SystemEnvironment.Instance.IsPlaceholderModeEnabled())
7877
{
79-
return new string[]
78+
if (SystemEnvironment.Instance.IsFlexConsumptionSku())
8079
{
81-
ScriptSettingsManager.Instance.GetSetting(WebsitePodName)
82-
};
80+
return new string[]
81+
{
82+
ScriptSettingsManager.Instance.GetSetting(WebsitePodName)
83+
};
84+
}
85+
else if (SystemEnvironment.Instance.IsLinuxConsumptionOnAtlas())
86+
{
87+
return new string[]
88+
{
89+
ScriptSettingsManager.Instance.GetSetting(ContainerName)
90+
};
91+
}
8392
}
8493

8594
return new string[]

src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ public static class SimpleWebTokenHelper
1515
/// <summary>
1616
/// A SWT or a Simple Web Token is a token that's made of key=value pairs separated
1717
/// by &. We only specify expiration in ticks from now (exp={ticks})
18-
/// The SWT is then returned as an encrypted string
18+
/// The SWT is then returned as an encrypted string.
1919
/// </summary>
20-
/// <param name="validUntil">Datetime for when the token should expire</param>
21-
/// <param name="key">Optional key to encrypt the token with</param>
22-
/// <returns>a SWT signed by this app</returns>
20+
/// <param name="validUntil">Datetime for when the token should expire.</param>
21+
/// <param name="key">Optional key to encrypt the token with.</param>
22+
/// <returns>a SWT signed by this app.</returns>
2323
public static string CreateToken(DateTime validUntil, byte[] key = null) => Encrypt($"exp={validUntil.Ticks}", key);
2424

25-
internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null)
25+
internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null, bool includesSignature = false)
2626
{
2727
key = key ?? SecretsUtility.GetEncryptionKey(environment);
2828

@@ -45,23 +45,31 @@ internal static string Encrypt(string value, byte[] key = null, IEnvironment env
4545
cryptoStream.FlushFinalBlock();
4646
}
4747

48-
// return {iv}.{swt}.{sha236(key)}
49-
return string.Format("{0}.{1}.{2}", iv, Convert.ToBase64String(cipherStream.ToArray()), GetSHA256Base64String(aes.Key));
48+
if (includesSignature)
49+
{
50+
return $"{Convert.ToBase64String(aes.IV)}.{Convert.ToBase64String(cipherStream.ToArray())}.{GetSHA256Base64String(aes.Key)}.{Convert.ToBase64String(ComputeHMACSHA256(aes.Key, input))}";
51+
}
52+
else
53+
{
54+
// return {iv}.{swt}.{sha236(key)}
55+
return string.Format("{0}.{1}.{2}", iv, Convert.ToBase64String(cipherStream.ToArray()), GetSHA256Base64String(aes.Key));
56+
}
5057
}
5158
}
5259
}
5360

5461
public static string Decrypt(byte[] encryptionKey, string value)
5562
{
5663
var parts = value.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
57-
if (parts.Length != 2 && parts.Length != 3)
64+
if (parts.Length != 2 && parts.Length != 3 && parts.Length != 4)
5865
{
5966
throw new InvalidOperationException("Malformed token.");
6067
}
6168

6269
var iv = Convert.FromBase64String(parts[0]);
6370
var data = Convert.FromBase64String(parts[1]);
6471
var base64KeyHash = parts.Length == 3 ? parts[2] : null;
72+
var signature = parts.Length == 4 ? Convert.FromBase64String(parts[3]) : null;
6573

6674
if (!string.IsNullOrEmpty(base64KeyHash) && !string.Equals(GetSHA256Base64String(encryptionKey), base64KeyHash))
6775
{
@@ -80,7 +88,13 @@ public static string Decrypt(byte[] encryptionKey, string value)
8088
binaryWriter.Write(data, 0, data.Length);
8189
}
8290

83-
return Encoding.UTF8.GetString(ms.ToArray());
91+
var input = ms.ToArray();
92+
if (signature != null && !signature.SequenceEqual(ComputeHMACSHA256(encryptionKey, input)))
93+
{
94+
throw new InvalidOperationException("Signature mismatches!");
95+
}
96+
97+
return Encoding.UTF8.GetString(input);
8498
}
8599
}
86100
}
@@ -125,5 +139,13 @@ private static string GetSHA256Base64String(byte[] key)
125139
return Convert.ToBase64String(sha256.ComputeHash(key));
126140
}
127141
}
142+
143+
private static byte[] ComputeHMACSHA256(byte[] key, byte[] input)
144+
{
145+
using (var hmacSha256 = new HMACSHA256(key))
146+
{
147+
return hmacSha256.ComputeHash(input);
148+
}
149+
}
128150
}
129151
}

src/WebJobs.Script/Config/ScriptSettingsManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public static ScriptSettingsManager Instance
3131
}
3232

3333
/// <summary>
34-
/// Gets a value indicating whether we are running in App Service
34+
/// Gets a value indicating whether we are running in App Service.
3535
/// </summary>
3636
public virtual bool IsAppServiceEnvironment => !string.IsNullOrEmpty(GetSetting(EnvironmentSettingNames.AzureWebsiteInstanceId));
3737

test/WebJobs.Script.Tests/Extensions/ScriptJwtBearerExtensionsTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using Microsoft.Azure.WebJobs.Script.Config;
88
using Microsoft.Extensions.DependencyInjection;
9+
using NuGet.Configuration;
910
using Xunit;
1011
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
1112

@@ -21,9 +22,11 @@ public class ScriptJwtBearerExtensionsTests
2122
public void CreateTokenValidationParameters_HasExpectedAudience(bool isPlaceholderModeEnabled, bool isLinuxConsumptionOnLegion)
2223
{
2324
var podName = "RandomPodName";
25+
var containerName = "RandomContainerName";
2426
var siteName = "RandomSiteName";
2527
ScriptSettingsManager.Instance.SetSetting(AzureWebsiteName, siteName);
2628
ScriptSettingsManager.Instance.SetSetting(WebsitePodName, podName);
29+
ScriptSettingsManager.Instance.SetSetting(ContainerName, string.Empty);
2730

2831
var expectedWithSiteName = new string[]
2932
{
@@ -34,6 +37,7 @@ public void CreateTokenValidationParameters_HasExpectedAudience(bool isPlacehold
3437
{
3538
ScriptSettingsManager.Instance.GetSetting(WebsitePodName)
3639
};
40+
var expectedWithContainerName = new string[] { };
3741

3842
var testData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
3943

@@ -48,6 +52,16 @@ public void CreateTokenValidationParameters_HasExpectedAudience(bool isPlacehold
4852
testData[WebsitePodName] = podName;
4953
testData[LegionServiceHost] = "1";
5054
}
55+
else
56+
{
57+
ScriptSettingsManager.Instance.SetSetting(ContainerName, containerName);
58+
expectedWithContainerName = new string[]
59+
{
60+
ScriptSettingsManager.Instance.GetSetting(ContainerName)
61+
};
62+
testData[AzureWebsiteInstanceId] = string.Empty;
63+
testData[ContainerName] = containerName;
64+
}
5165

5266
testData[ContainerEncryptionKey] = Convert.ToBase64String(TestHelpers.GenerateKeyBytes());
5367
using (new TestScopedEnvironmentVariable(testData))
@@ -61,6 +75,11 @@ public void CreateTokenValidationParameters_HasExpectedAudience(bool isPlacehold
6175
Assert.Equal(audiences.Count, expectedWithPodName.Length);
6276
Assert.Equal(audiences[0], expectedWithPodName[0]);
6377
}
78+
else if (isPlaceholderModeEnabled)
79+
{
80+
Assert.Equal(audiences.Count, expectedWithContainerName.Length);
81+
Assert.Equal(audiences[0], expectedWithContainerName[0]);
82+
}
6483
else
6584
{
6685
Assert.Equal(audiences.Count, expectedWithSiteName.Length);

test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.Azure.WebJobs.Script.WebHost;
8+
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
69
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
10+
using Newtonsoft.Json;
711
using Xunit;
812

913
namespace Microsoft.Azure.WebJobs.Script.Tests.Helpers
@@ -89,11 +93,63 @@ public void Validate_Token_Uses_Website_Encryption_Key_If_Container_Encryption_K
8993
Assert.True(SimpleWebTokenHelper.TryValidateToken(token, new SystemClock()));
9094
}
9195

96+
[Fact]
97+
public void Validate_Token_Checks_Signature_If_Signature_Is_Available()
98+
{
99+
var websiteAuthEncryptionKey = TestHelpers.GenerateKeyBytes();
100+
var websiteAuthEncryptionStringKey = TestHelpers.GenerateKeyHexString(websiteAuthEncryptionKey);
101+
102+
var timeStamp = DateTime.UtcNow.AddHours(1);
103+
104+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
105+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, websiteAuthEncryptionStringKey);
106+
107+
var token = SimpleWebTokenHelper.Encrypt($"exp={timeStamp.Ticks}", websiteAuthEncryptionKey, includesSignature: true);
108+
109+
Assert.True(SimpleWebTokenHelper.TryValidateToken(token, new SystemClock()));
110+
}
111+
112+
[Fact]
113+
public void Encrypt_And_Decrypt_Context_With_Signature()
114+
{
115+
var websiteAuthEncryptionKey = TestHelpers.GenerateKeyBytes();
116+
var websiteAuthEncryptionStringKey = TestHelpers.GenerateKeyHexString(websiteAuthEncryptionKey);
117+
var hostContext = GetHostAssignmentContext();
118+
var hostContextJson = JsonConvert.SerializeObject(hostContext);
119+
120+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
121+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, websiteAuthEncryptionStringKey);
122+
123+
var encryptedHostContextWithSignature = SimpleWebTokenHelper.Encrypt(hostContextJson, websiteAuthEncryptionKey, includesSignature: true);
124+
125+
var decryptedHostContextJson = SimpleWebTokenHelper.Decrypt(websiteAuthEncryptionKey, encryptedHostContextWithSignature);
126+
127+
Assert.Equal(hostContextJson, decryptedHostContextJson);
128+
}
129+
92130
public void Dispose()
93131
{
94132
// Clean up
95133
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, string.Empty);
96134
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
97135
}
136+
137+
private static HostAssignmentContext GetHostAssignmentContext()
138+
{
139+
var hostAssignmentContext = new HostAssignmentContext();
140+
hostAssignmentContext.SiteId = 1;
141+
hostAssignmentContext.SiteName = "sitename";
142+
hostAssignmentContext.LastModifiedTime = DateTime.UtcNow.Add(TimeSpan.FromMinutes(new Random().Next()));
143+
hostAssignmentContext.Environment = new Dictionary<string, string>();
144+
hostAssignmentContext.MSIContext = new MSIContext();
145+
hostAssignmentContext.EncryptedTokenServiceSpecializationPayload = "payload";
146+
hostAssignmentContext.TokenServiceApiEndpoint = "endpoints";
147+
hostAssignmentContext.CorsSettings = new CorsSettings();
148+
hostAssignmentContext.EasyAuthSettings = new EasyAuthSettings();
149+
hostAssignmentContext.Secrets = new FunctionAppSecrets();
150+
hostAssignmentContext.IsWarmupRequest = false;
151+
152+
return hostAssignmentContext;
153+
}
98154
}
99155
}

0 commit comments

Comments
 (0)