Skip to content

Commit 138a399

Browse files
authored
Add identifiable secrets generation using Microsoft.Security.Utilities (michaelcfanning) (#8202)
1 parent db5beec commit 138a399

File tree

9 files changed

+378
-118
lines changed

9 files changed

+378
-118
lines changed

src/WebJobs.Script.WebHost/Security/KeyManagement/SecretManager.cs

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
using Microsoft.Azure.WebJobs.Extensions.Http;
1616
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1717
using Microsoft.Azure.WebJobs.Script.WebHost.Properties;
18+
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
1819
using Microsoft.Extensions.Logging;
19-
using DataProtectionCostants = Microsoft.Azure.Web.DataProtection.Constants;
20+
21+
using DataProtectionConstants = Microsoft.Azure.Web.DataProtection.Constants;
2022

2123
namespace Microsoft.Azure.WebJobs.Script.WebHost
2224
{
@@ -254,7 +256,7 @@ public async Task<KeyOperationResult> SetMasterKeyAsync(string value = null)
254256
if (value == null)
255257
{
256258
// Generate a new secret (clear)
257-
masterKey = GenerateSecret();
259+
masterKey = SecretGenerator.GenerateMasterKeyValue();
258260
result = OperationResult.Created;
259261
}
260262
else
@@ -303,7 +305,7 @@ private async Task<KeyOperationResult> AddOrUpdateSecretAsync(ScriptSecretsType
303305
{
304306
OperationResult result = OperationResult.NotFound;
305307

306-
secret = secret ?? GenerateSecret();
308+
secret = secret ?? SecretGenerator.GenerateFunctionKeyValue();
307309

308310
await ModifyFunctionSecretsAsync(secretsType, keyScope, secrets =>
309311
{
@@ -493,10 +495,10 @@ private HostSecrets GenerateHostSecrets()
493495
{
494496
return new HostSecrets
495497
{
496-
MasterKey = GenerateKey(ScriptConstants.DefaultMasterKeyName),
498+
MasterKey = GenerateMasterKey(),
497499
FunctionKeys = new List<Key>
498500
{
499-
GenerateKey(ScriptConstants.DefaultFunctionKeyName)
501+
GenerateFunctionKey(),
500502
},
501503
SystemKeys = new List<Key>()
502504
};
@@ -506,10 +508,10 @@ private HostSecrets GenerateHostSecrets(HostSecrets secrets)
506508
{
507509
if (secrets.MasterKey.IsEncrypted)
508510
{
509-
secrets.MasterKey.Value = GenerateSecret();
511+
secrets.MasterKey.Value = SecretGenerator.GenerateMasterKeyValue();
510512
}
511-
secrets.SystemKeys = RegenerateKeys(secrets.SystemKeys);
512-
secrets.FunctionKeys = RegenerateKeys(secrets.FunctionKeys);
513+
secrets.SystemKeys = RegenerateKeys(secrets.SystemKeys, SecretGenerator.SystemKeySeed);
514+
secrets.FunctionKeys = RegenerateKeys(secrets.FunctionKeys, SecretGenerator.FunctionKeySeed);
513515
return secrets;
514516
}
515517

@@ -519,24 +521,24 @@ private FunctionSecrets GenerateFunctionSecrets()
519521
{
520522
Keys = new List<Key>
521523
{
522-
GenerateKey(ScriptConstants.DefaultFunctionKeyName)
524+
GenerateFunctionKey()
523525
}
524526
};
525527
}
526528

527529
private FunctionSecrets GenerateFunctionSecrets(FunctionSecrets secrets)
528530
{
529-
secrets.Keys = RegenerateKeys(secrets.Keys);
531+
secrets.Keys = RegenerateKeys(secrets.Keys, SecretGenerator.FunctionKeySeed);
530532
return secrets;
531533
}
532534

533-
private IList<Key> RegenerateKeys(IList<Key> list)
535+
private IList<Key> RegenerateKeys(IList<Key> list, ulong seed)
534536
{
535537
return list.Select(k =>
536538
{
537539
if (k.IsEncrypted)
538540
{
539-
k.Value = GenerateSecret();
541+
k.Value = SecretGenerator.GenerateIdentifiableSecret(seed);
540542
}
541543
return k;
542544
}).ToList();
@@ -594,31 +596,32 @@ private HostSecrets ReadHostSecrets(HostSecrets hostSecrets)
594596
};
595597
}
596598

597-
private Key GenerateKey(string name = null)
599+
private Key GenerateMasterKey()
598600
{
599-
string secret = GenerateSecret();
601+
string secret = SecretGenerator.GenerateMasterKeyValue();
600602

601-
return CreateKey(name, secret);
603+
return CreateKey(ScriptConstants.DefaultMasterKeyName, secret);
602604
}
603605

604-
private Key CreateKey(string name, string secret)
606+
private Key GenerateFunctionKey()
605607
{
606-
var key = new Key(name, secret);
608+
string secret = SecretGenerator.GenerateFunctionKeyValue();
607609

608-
return _keyValueConverterFactory.WriteKey(key);
610+
return CreateKey(ScriptConstants.DefaultFunctionKeyName, secret);
609611
}
610612

611-
internal static string GenerateSecret()
613+
private Key CreateKey(string name, ulong seed)
612614
{
613-
using (var rng = RandomNumberGenerator.Create())
614-
{
615-
byte[] data = new byte[40];
616-
rng.GetBytes(data);
617-
string secret = Convert.ToBase64String(data);
615+
string secret = SecretGenerator.GenerateIdentifiableSecret(seed);
618616

619-
// Replace pluses as they are problematic as URL values
620-
return secret.Replace('+', 'a');
621-
}
617+
return CreateKey(name, secret);
618+
}
619+
620+
private Key CreateKey(string name, string secret)
621+
{
622+
var key = new Key(name, secret);
623+
624+
return _keyValueConverterFactory.WriteKey(key);
622625
}
623626

624627
private void OnSecretsChanged(object sender, SecretsChangedEventArgs e)
@@ -725,22 +728,22 @@ private void InitializeCache()
725728
private string GetEncryptionKeysHashes()
726729
{
727730
string result = string.Empty;
728-
string azureWebsiteLocalEncryptionKey = SystemEnvironment.Instance.GetEnvironmentVariable(DataProtectionCostants.AzureWebsiteLocalEncryptionKey) ?? string.Empty;
731+
string azureWebsiteLocalEncryptionKey = SystemEnvironment.Instance.GetEnvironmentVariable(DataProtectionConstants.AzureWebsiteLocalEncryptionKey) ?? string.Empty;
729732
SHA256Managed hash = new SHA256Managed();
730733

731734
if (!string.IsNullOrEmpty(azureWebsiteLocalEncryptionKey))
732735
{
733736
byte[] hashBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(azureWebsiteLocalEncryptionKey));
734737
string azureWebsiteLocalEncryptionKeyHash = Convert.ToBase64String(hashBytes);
735-
result += $"{DataProtectionCostants.AzureWebsiteLocalEncryptionKey}={azureWebsiteLocalEncryptionKeyHash};";
738+
result += $"{DataProtectionConstants.AzureWebsiteLocalEncryptionKey}={azureWebsiteLocalEncryptionKeyHash};";
736739
}
737740

738-
string azureWebsiteEnvironmentMachineKey = SystemEnvironment.Instance.GetEnvironmentVariable(DataProtectionCostants.AzureWebsiteEnvironmentMachineKey) ?? string.Empty;
741+
string azureWebsiteEnvironmentMachineKey = SystemEnvironment.Instance.GetEnvironmentVariable(DataProtectionConstants.AzureWebsiteEnvironmentMachineKey) ?? string.Empty;
739742
if (!string.IsNullOrEmpty(azureWebsiteEnvironmentMachineKey))
740743
{
741744
byte[] hashBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(azureWebsiteEnvironmentMachineKey));
742745
string azureWebsiteEnvironmentMachineKeyHash = Convert.ToBase64String(hashBytes);
743-
result += $"{DataProtectionCostants.AzureWebsiteEnvironmentMachineKey}={azureWebsiteEnvironmentMachineKeyHash};";
746+
result += $"{DataProtectionConstants.AzureWebsiteEnvironmentMachineKey}={azureWebsiteEnvironmentMachineKeyHash};";
744747
}
745748

746749
return result;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Security.Utilities;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Security
7+
{
8+
/// <summary>
9+
/// Generates identifiable secret values for use as Azure Functions keys.
10+
/// </summary>
11+
public static class SecretGenerator
12+
{
13+
public const string AzureFunctionsSignature = "AzFu";
14+
15+
// Seeds (passed to the Marvin checksum algorithm) for grouping
16+
// Azure Functions Host keys. See references from unit tests for
17+
// information on how these seeds were generated/are versioned.
18+
public const ulong MasterKeySeed = 0x4d61737465723030;
19+
public const ulong SystemKeySeed = 0x53797374656d3030;
20+
public const ulong FunctionKeySeed = 0x46756e6374693030;
21+
22+
public static string GenerateMasterKeyValue()
23+
{
24+
return GenerateIdentifiableSecret(MasterKeySeed);
25+
}
26+
27+
public static string GenerateFunctionKeyValue()
28+
{
29+
return GenerateIdentifiableSecret(FunctionKeySeed);
30+
}
31+
32+
public static string GenerateSystemKeyValue()
33+
{
34+
return GenerateIdentifiableSecret(SystemKeySeed);
35+
}
36+
37+
internal static string GenerateIdentifiableSecret(ulong seed)
38+
{
39+
// Return a generated secret with a completely URL-safe base64-encoding
40+
// alphabet, 'a-zA-Z0-9' as well as '-' and '_'. We preserve the trailing
41+
// equal sign padding in the token in order to improve the ability to
42+
// match against the general token format (with performing a checksum
43+
// validation. This is safe in Azure Functions utilization because a
44+
// token will only appear in a URL as a query string parameter, where
45+
// equal signs do not require encoding.
46+
return IdentifiableSecrets.GenerateUrlSafeBase64Key(seed, 40, AzureFunctionsSignature, elidePadding: false);
47+
}
48+
}
49+
}

src/WebJobs.Script.WebHost/WebHooks/DefaultScriptWebHookProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Azure.WebJobs.Description;
99
using Microsoft.Azure.WebJobs.Host.Config;
1010
using Microsoft.Azure.WebJobs.Script.Config;
11+
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
1112
using HttpHandler = Microsoft.Azure.WebJobs.IAsyncConverter<System.Net.Http.HttpRequestMessage, System.Net.Http.HttpResponseMessage>;
1213

1314
namespace Microsoft.Azure.WebJobs.Script.WebHost
@@ -77,7 +78,7 @@ private async Task<string> GetOrCreateExtensionKey(string extensionName)
7778
if (!hostSecrets.SystemKeys.TryGetValue(keyName, out keyValue))
7879
{
7980
// if the requested secret doesn't exist, create it on demand
80-
keyValue = SecretManager.GenerateSecret();
81+
keyValue = SecretGenerator.GenerateSystemKeyValue();
8182
await secretManager.AddOrUpdateFunctionSecretAsync(keyName, keyValue, HostKeyScopes.SystemKeys, ScriptSecretsType.Host);
8283
}
8384

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<PackageReference Include="Microsoft.Azure.WebJobs.Logging" Version="4.0.2" />
8080
<PackageReference Include="Microsoft.Azure.WebSites.DataProtection" Version="2.1.91-alpha" />
8181
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.20.0" />
82+
<PackageReference Include="Microsoft.Security.Utilities" Version="1.3.0" />
8283
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.3" />
8384
<PackageReference Include="protobuf-net" Version="3.0.0" />
8485
<!-- The System.Interactive.Async assembly is required to be deployed. See https://github.com/Azure/azure-functions-host/issues/6203 -->

src/WebJobs.Script/runtimeassemblies.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,10 @@
13781378
"name": "System.Transactions.Local",
13791379
"resolutionPolicy": "minorMatchOrLower"
13801380
},
1381+
{
1382+
"name": "Microsoft.Security.Utilities",
1383+
"resolutionPolicy": "private"
1384+
},
13811385
{
13821386
"name": "System.ValueTuple",
13831387
"resolutionPolicy": "minorMatchOrLower"

test/WebJobs.Script.Tests/DefaultScriptWebHookProviderTests.cs

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
using Microsoft.Azure.WebJobs.Description;
99
using Microsoft.Azure.WebJobs.Host.Config;
1010
using Microsoft.Azure.WebJobs.Script.WebHost;
11+
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
1112
using Microsoft.Extensions.Logging;
13+
using Microsoft.Security.Utilities;
1214
using Microsoft.WebJobs.Script.Tests;
1315
using Moq;
1416
using Xunit;
@@ -18,40 +20,72 @@ namespace Microsoft.Azure.WebJobs.Script.Tests
1820
public class DefaultScriptWebHookProviderTests
1921
{
2022
private const string TestHostName = "test.azurewebsites.net";
21-
22-
private readonly HostSecretsInfo _hostSecrets;
23-
private readonly Mock<ISecretManager> _mockSecretManager;
24-
private readonly IScriptWebHookProvider _webHookProvider;
25-
26-
public DefaultScriptWebHookProviderTests()
27-
{
28-
_mockSecretManager = new Mock<ISecretManager>(MockBehavior.Strict);
29-
_hostSecrets = new HostSecretsInfo();
30-
_mockSecretManager.Setup(p => p.GetHostSecretsAsync()).ReturnsAsync(_hostSecrets);
31-
var mockSecretManagerProvider = new Mock<ISecretManagerProvider>(MockBehavior.Strict);
32-
mockSecretManagerProvider.Setup(p => p.Current).Returns(_mockSecretManager.Object);
33-
var loggerProvider = new TestLoggerProvider();
34-
var loggerFactory = new LoggerFactory();
35-
loggerFactory.AddProvider(loggerProvider);
36-
var testEnvironment = new TestEnvironment();
37-
testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHostName, TestHostName);
38-
var hostNameProvider = new HostNameProvider(testEnvironment);
39-
_webHookProvider = new DefaultScriptWebHookProvider(mockSecretManagerProvider.Object, hostNameProvider);
40-
}
23+
private const string TestUrlRoot = "https://test.azurewebsites.net/runtime/webhooks/testextension?code=";
4124

4225
[Fact]
4326
public void GetUrl_ReturnsExpectedResult()
4427
{
45-
_hostSecrets.SystemKeys = new Dictionary<string, string>
28+
var webHookProvider = CreateDefaultScriptWebHookProvider(out Mock<ISecretManager> mockSecretManager, out HostSecretsInfo hostSecrets);
29+
mockSecretManager.Setup(p => p.GetHostSecretsAsync()).ReturnsAsync(hostSecrets);
30+
31+
// When an extension has an existing secret, it should be returned.
32+
hostSecrets.SystemKeys = new Dictionary<string, string>
4633
{
4734
{ "testextension_extension", "abc123" }
4835
};
4936

5037
var configProvider = new TestExtensionConfigProvider();
51-
var url = _webHookProvider.GetUrl(configProvider);
38+
var url = webHookProvider.GetUrl(configProvider);
5239
Assert.Equal("https://test.azurewebsites.net/runtime/webhooks/testextension?code=abc123", url.ToString());
5340
}
5441

42+
[Fact]
43+
public void GetUrl_GeneratesIdentifiableSystemSecret()
44+
{
45+
string secretValue = string.Empty;
46+
47+
var webHookProvider = CreateDefaultScriptWebHookProvider(out Mock<ISecretManager> mockSecretManager, out HostSecretsInfo hostSecrets);
48+
mockSecretManager.Setup(p => p.GetHostSecretsAsync()).ReturnsAsync(hostSecrets);
49+
50+
mockSecretManager.Setup(p =>
51+
p.AddOrUpdateFunctionSecretAsync(
52+
"testextension_extension",
53+
It.IsAny<string>(),
54+
HostKeyScopes.SystemKeys,
55+
ScriptSecretsType.Host))
56+
.Callback<string, string, string, ScriptSecretsType>((key, secret, scope, type) => secretValue = secret)
57+
.Returns(() => Task.FromResult(new KeyOperationResult(secretValue, OperationResult.Created)));
58+
59+
// When an extension has no existing secret, one should be generated using
60+
// the Azure Functions system key seed and standard fixed signature.
61+
62+
var configProvider = new TestExtensionConfigProvider();
63+
var url = webHookProvider.GetUrl(configProvider);
64+
Assert.Equal($"{TestUrlRoot}{secretValue}", url.ToString());
65+
Assert.True(IdentifiableSecrets.ValidateBase64Key(secretValue,
66+
SecretGenerator.SystemKeySeed,
67+
SecretGenerator.AzureFunctionsSignature,
68+
encodeForUrl: true));
69+
}
70+
71+
private static DefaultScriptWebHookProvider CreateDefaultScriptWebHookProvider(out Mock<ISecretManager> mockSecretManager, out HostSecretsInfo hostSecrets)
72+
{
73+
mockSecretManager = new Mock<ISecretManager>(MockBehavior.Strict);
74+
hostSecrets = new HostSecretsInfo();
75+
hostSecrets.SystemKeys = new Dictionary<string, string>();
76+
hostSecrets.FunctionKeys = new Dictionary<string, string>();
77+
mockSecretManager.Setup(p => p.GetHostSecretsAsync()).ReturnsAsync(hostSecrets);
78+
var mockSecretManagerProvider = new Mock<ISecretManagerProvider>(MockBehavior.Strict);
79+
mockSecretManagerProvider.Setup(p => p.Current).Returns(mockSecretManager.Object);
80+
var loggerProvider = new TestLoggerProvider();
81+
var loggerFactory = new LoggerFactory();
82+
loggerFactory.AddProvider(loggerProvider);
83+
var testEnvironment = new TestEnvironment();
84+
testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHostName, TestHostName);
85+
var hostNameProvider = new HostNameProvider(testEnvironment);
86+
return new DefaultScriptWebHookProvider(mockSecretManagerProvider.Object, hostNameProvider);
87+
}
88+
5589
[Extension("My Test Extension", configurationSection: "TestExtension")]
5690
private class TestExtensionConfigProvider : IExtensionConfigProvider, IAsyncConverter<HttpRequestMessage, HttpResponseMessage>
5791
{

0 commit comments

Comments
 (0)