diff --git a/Scripts/PythonScript/example_ase.py b/Scripts/PythonScript/example_ase.py
index 020ef49..76d0204 100644
--- a/Scripts/PythonScript/example_ase.py
+++ b/Scripts/PythonScript/example_ase.py
@@ -18,6 +18,7 @@
import traceback
import uuid
import sys
+from datetime import datetime, timedelta
from azure.common.client_factory import get_client_from_cli_profile
from azure.common.credentials import ServicePrincipalCredentials
@@ -181,12 +182,11 @@ def run_example():
blob_endpoint=storage_endpoint_suffix,
)
- # Create a container called 'vmimages'.
- # Set the permission so the blobs are public.
- # Upload the created file, use vhd_file_name for the blob name.
+ # Create a private container and upload the VHD.
+ # Generate a short-lived SAS URL for image creation instead of exposing the blob publicly.
print("\nUploading to Azure Stack Storage as blob:\n\t" + vhd_file_name)
blob_client = PageBlobService(connection_string=connection_string)
- container_client = blob_client.create_container(container_name, public_access='container', fail_on_exist=False)
+ container_client = blob_client.create_container(container_name, fail_on_exist=False)
blob_client.create_blob_from_path(container_name, vhd_file_name, vhd_file_path)
# List the blobs in the container
@@ -195,8 +195,13 @@ def run_example():
for blob in blob_list:
print("\t" + blob.name)
- # Construct the blob uri so it can be used to create the VM image
- blob_uri = urlparse(arm_url).scheme + '://' + blob_client.primary_endpoint + '/' + container_name + '/' + vhd_file_name
+ # Construct a short-lived SAS URI so it can be used to create the VM image.
+ sas_token = blob_client.generate_blob_shared_access_signature(
+ container_name,
+ vhd_file_name,
+ permission='r',
+ expiry=datetime.utcnow() + timedelta(hours=1))
+ blob_uri = urlparse(arm_url).scheme + '://' + blob_client.primary_endpoint + '/' + container_name + '/' + vhd_file_name + '?' + sas_token
# Create image from the VHD
async_creation = compute_client.images.create_or_update(
diff --git a/docs/ase-sample-explained.md b/docs/ase-sample-explained.md
new file mode 100644
index 0000000..66c1726
--- /dev/null
+++ b/docs/ase-sample-explained.md
@@ -0,0 +1,214 @@
+# Azure Stack Edge (ASE) VM Sample — Explained Simply
+
+New to Azure Stack Edge? Start here. This doc explains, in plain language, what the
+affected sample does, why it exists, and what the recent security fixes changed.
+
+---
+
+## 1. What is Azure Stack Edge, in one picture?
+
+Azure Stack Edge (ASE) is a **physical appliance** Microsoft ships to your location
+(a factory, a store, a ship, a remote site). It runs a small slice of Azure **locally**,
+so you can run virtual machines (VMs) and containers close to your data — even with poor
+or no internet.
+
+Think of it as **"a tiny piece of Azure that lives in your building."**
+
+```mermaid
+flowchart LR
+ subgraph Cloud["☁️ Azure Cloud (far away)"]
+ Portal["Azure Portal / Cloud ARM"]
+ end
+ subgraph Edge["🏭 Your Site (local)"]
+ ASE["Azure Stack Edge device
(runs VMs locally)"]
+ VM["Your Virtual Machine"]
+ end
+ Portal -. "register / manage" .-> ASE
+ ASE --> VM
+```
+
+Key idea: the device has its **own local ARM** (Azure Resource Manager). ARM is the
+"control panel API" of Azure. Normally ARM lives in the cloud; on ASE, a copy runs
+**on the box itself** so you can create resources locally.
+
+---
+
+## 2. What does this sample actually do?
+
+The sample is a small program that **creates a virtual machine on the ASE device**.
+
+It does not touch the public cloud to create the VM — it talks to the **local ARM
+endpoint on the appliance**. The sample exists to show developers the exact steps and
+API calls needed to automate VM creation on ASE.
+
+There are several flavors of the same idea in this repo:
+
+| Flavor | Folder | Language |
+|--------|--------|----------|
+| Local ARM (the one these fixes touch) | `dotnetSamples/LocalArm/CreateVmSample` | C# / .NET |
+| Python script | `Scripts/PythonScript/example_ase.py` | Python |
+| PowerShell | `Scripts/AzPowerShellScript` | PowerShell |
+
+This doc focuses on the **C# LocalArm sample** and the **Python sample**, because those
+are the two the security fixes changed.
+
+---
+
+## 3. The steps the sample performs
+
+At a high level, creating a VM on ASE looks like this:
+
+```mermaid
+flowchart TD
+ A["1. Sign in to the device
(get an access token)"] --> B["2. Create a resource group"]
+ B --> C["3. Upload a VHD
(the VM's disk image)"]
+ C --> D["4. Create an Image from the VHD"]
+ D --> E["5. Create a network card (NIC)"]
+ E --> F["6. Create the Virtual Machine"]
+ F --> G["✅ VM running on the ASE device"]
+```
+
+A **VHD** is just a file that contains a ready-made operating system disk (for example,
+an Ubuntu or Windows disk). You upload it to the device's local storage, turn it into an
+**image**, and then create VMs from that image.
+
+---
+
+## 4. How sign-in works (and where the certificate fits)
+
+To talk to the device, the sample must first **prove who it is** and get a token. It
+contacts two HTTPS endpoints **on the appliance**:
+
+- the **login endpoint** (ADFS): `https://login.dbe-.microsoftdatabox.com/adfs/`
+- the **management endpoint** (local ARM): `https://management.dbe-.microsoftdatabox.com/`
+
+```mermaid
+sequenceDiagram
+ participant App as Sample program
+ participant Login as ASE login endpoint (HTTPS)
+ participant ARM as ASE management endpoint (HTTPS)
+
+ App->>Login: username + password
+ Login-->>App: access token 🎟️
+ App->>ARM: "create VM" + token
+ ARM-->>App: VM created ✅
+```
+
+Because these calls are **HTTPS**, the device presents a **TLS certificate** — a digital
+ID card that proves "you are really talking to the appliance and not an imposter." Your
+computer normally checks that ID card automatically.
+
+---
+
+## 5. The security problem that was fixed (certificate change)
+
+### What the old code did
+
+The original sample **turned off certificate checking completely** — for the whole
+program. It effectively said: *"accept any HTTPS certificate, always, no questions asked."*
+
+```mermaid
+flowchart LR
+ App["Sample program"] -->|"accepts ANY certificate 😬"| Anything["Any server
(real device OR imposter)"]
+```
+
+That made setup easy, but it is dangerous: if an attacker sat between you and the device,
+your program would happily send your **username, password, and access token** to the
+attacker, because it never verified who it was talking to. This is called a
+**man-in-the-middle (MITM)** attack.
+
+### What the fix does
+
+The blanket "accept everything" switch is **removed**. Now:
+
+```mermaid
+flowchart TD
+ Start["Sample makes HTTPS call"] --> Q{"Is the certificate
valid & trusted?"}
+ Q -->|Yes| OK["✅ Connect"]
+ Q -->|No| Q2{"Did you provide a
specific thumbprint
to trust?"}
+ Q2 -->|"No thumbprint"| Fail["❌ Connection fails (safe default)"]
+ Q2 -->|"Thumbprint set"| Q3{"Does the device's
certificate match that
exact thumbprint
AND host?"}
+ Q3 -->|Yes| OK
+ Q3 -->|No| Fail
+```
+
+Two ways to connect now, both safe:
+
+1. **Normal (recommended):** trust the appliance certificate properly on your machine
+ (install the device/root certificate), then the sample "just works" with full validation.
+2. **Pinned exception (opt-in):** if you must trust a specific device certificate (common
+ in labs or private setups), you provide its **thumbprint** — a unique fingerprint of
+ that one certificate. The sample will then trust **only** that exact certificate, and
+ **only** for the ASE login/management hosts. Everything else is still rejected.
+
+A **thumbprint** is like the certificate's unique serial number. Trusting one thumbprint
+is very different from "trust everything" — it is a single, specific ID you have chosen in
+advance.
+
+### How you use the opt-in in the sample
+
+In `Program.cs` there is now a clearly labeled field:
+
+```csharp
+// Optional appliance certificate thumbprint.
+// Leave this blank to use normal TLS validation.
+// Set it only when you need to trust a specific ASE certificate.
+var applianceCertificateThumbprint = "";
+```
+
+- Leave it **empty** → normal, full certificate validation.
+- Paste the device's certificate **thumbprint** → trust only that certificate for the ASE
+ endpoints.
+
+You can read the thumbprint from the ASE local UI, or with:
+`openssl s_client -connect management.dbe-.microsoftdatabox.com:443`
+
+---
+
+## 6. The second fix (Python storage change)
+
+The Python sample uploads the VHD to a storage container on the device. The old code made
+that container **public** — anyone who could reach it could read the VM disk image.
+
+```mermaid
+flowchart LR
+ subgraph Before["❌ Before"]
+ B1["VHD in PUBLIC container"] -->|"anyone can read"| B2["🌐 Any anonymous user"]
+ end
+ subgraph After["✅ After"]
+ A1["VHD in PRIVATE container"] -->|"short-lived read link (SAS)"| A2["Only the image-create step"]
+ end
+```
+
+The fix keeps the container **private** and instead generates a **SAS** (Shared Access
+Signature) — a temporary, read-only link that expires after one hour and is only used to
+create the image. No more public exposure of your disk image.
+
+---
+
+## 7. How this is tested
+
+The certificate-pinning logic now has **automated unit tests** (MSTest) that verify:
+
+- with no errors, a valid certificate is accepted;
+- a matching thumbprint on an allowed ASE host is accepted, even with a chain warning;
+- a **wrong** thumbprint, a **wrong** host, a **missing** certificate, or an **unknown
+ caller type** are all rejected.
+
+This proves the safe behavior holds without needing a physical device.
+
+```mermaid
+flowchart LR
+ T["Unit tests"] --> V["ValidatePinnedCertificate()"]
+ V --> Pass["✅ correct cert + host"]
+ V --> Reject["❌ wrong cert / wrong host / no cert"]
+```
+
+---
+
+## 8. Summary in one line
+
+The sample **creates a VM on your local Azure Stack Edge box**; the fixes make it **verify
+who it is talking to** (instead of trusting any certificate) and **stop exposing the VM
+disk image publicly** — while still giving you a safe, opt-in way to trust a specific
+device certificate.
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/CertificatePinningTests.cs b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/CertificatePinningTests.cs
new file mode 100644
index 0000000..9a61535
--- /dev/null
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/CertificatePinningTests.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Security;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+using CreateVmSample;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CreateVmSample.Tests
+{
+ [TestClass]
+ public class CertificatePinningTests
+ {
+ private const string ManagementUrl = "https://management.dbe-appliance.microsoftdatabox.com/";
+ private const string AuthorityUrl = "https://login.dbe-appliance.microsoftdatabox.com/adfs/";
+ private const string ManagementHost = "management.dbe-appliance.microsoftdatabox.com";
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ AseServiceClientCredentials.ResetCertificateValidationForTest();
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ AseServiceClientCredentials.ResetCertificateValidationForTest();
+ }
+
+ private static X509Certificate2 CreateSelfSignedCertificate(string subjectName = "CN=ase-pinning-test")
+ {
+ using (var rsa = RSA.Create(2048))
+ {
+ var request = new CertificateRequest(
+ subjectName,
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ return request.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(1));
+ }
+ }
+
+ [TestMethod]
+ public void NormalizeThumbprint_StripsSeparatorsAndUppercases()
+ {
+ var normalizedWithSeparators = AseServiceClientCredentials.NormalizeThumbprint("ab:cd ef");
+ var normalizedPlain = AseServiceClientCredentials.NormalizeThumbprint("ABCDEF");
+
+ Assert.AreEqual("ABCDEF", normalizedWithSeparators);
+ Assert.AreEqual(normalizedPlain, normalizedWithSeparators);
+ }
+
+ [TestMethod]
+ public void NormalizeThumbprint_NullOrWhitespace_ReturnsNull()
+ {
+ Assert.IsNull(AseServiceClientCredentials.NormalizeThumbprint(null));
+ Assert.IsNull(AseServiceClientCredentials.NormalizeThumbprint(string.Empty));
+ Assert.IsNull(AseServiceClientCredentials.NormalizeThumbprint(" "));
+ }
+
+ [TestMethod]
+ public void Validate_NoSslErrors_ReturnsTrue()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ ManagementHost,
+ cert,
+ null,
+ SslPolicyErrors.None);
+
+ Assert.IsTrue(result);
+ }
+ }
+
+ [TestMethod]
+ public void Validate_MatchingThumbprint_AllowedHost_ChainError_ReturnsTrue()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ cert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ ManagementHost,
+ cert,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsTrue(result);
+ }
+ }
+
+ [TestMethod]
+ public void Validate_MatchingThumbprint_DisallowedHost_ReturnsFalse()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ cert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ "evil.contoso.com",
+ cert,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsFalse(result);
+ }
+ }
+
+ [TestMethod]
+ public void Validate_NonMatchingThumbprint_AllowedHost_ReturnsFalse()
+ {
+ using (var configuredCert = CreateSelfSignedCertificate("CN=ase-pinning-configured"))
+ using (var presentedCert = CreateSelfSignedCertificate("CN=ase-pinning-presented"))
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ configuredCert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ ManagementHost,
+ presentedCert,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsFalse(result);
+ }
+ }
+
+ [TestMethod]
+ public void Validate_NullCertificate_ReturnsFalse()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ cert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ ManagementHost,
+ null,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsFalse(result);
+ }
+ }
+
+ [TestMethod]
+ public void Validate_HttpWebRequestSender_HostExtracted_ReturnsTrue()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ cert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ var sender = (HttpWebRequest)WebRequest.Create(ManagementUrl);
+
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ sender,
+ cert,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsTrue(result);
+ }
+ }
+
+ [TestMethod]
+ public void Validate_HttpRequestMessageSender_HostExtracted_ReturnsTrue()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ cert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ using (var sender = new HttpRequestMessage(HttpMethod.Get, ManagementUrl))
+ {
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ sender,
+ cert,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsTrue(result);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void Validate_UnknownSenderType_ReturnsFalse()
+ {
+ using (var cert = CreateSelfSignedCertificate())
+ {
+ AseServiceClientCredentials.ConfigureCertificateValidationForTest(
+ cert.GetCertHashString(),
+ ManagementUrl,
+ AuthorityUrl);
+
+ var result = AseServiceClientCredentials.ValidatePinnedCertificate(
+ new object(),
+ cert,
+ null,
+ SslPolicyErrors.RemoteCertificateChainErrors);
+
+ Assert.IsFalse(result);
+ }
+ }
+ }
+}
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/CreateVmSample.Tests.csproj b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/CreateVmSample.Tests.csproj
new file mode 100644
index 0000000..757a6e4
--- /dev/null
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/CreateVmSample.Tests.csproj
@@ -0,0 +1,80 @@
+
+
+
+
+
+ Debug
+ AnyCPU
+ {7F3C9A21-4E2B-4C7D-9F1A-2B6E5D8C0A11}
+ Library
+ CreateVmSample.Tests
+ CreateVmSample.Tests
+ v4.7.2
+ 512
+ true
+ true
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
+
+
+ ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
+
+
+ ..\packages\Microsoft.Rest.ClientRuntime.2.3.19\lib\net461\Microsoft.Rest.ClientRuntime.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {95737F72-6C25-4AE3-98D2-FC0258A0F612}
+ CreateVmSample
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/Properties/AssemblyInfo.cs b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..bfc9e25
--- /dev/null
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("CreateVmSample.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("CreateVmSample.Tests")]
+[assembly: AssemblyCopyright("Copyright © 2026")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+
+[assembly: Guid("7f3c9a21-4e2b-4c7d-9f1a-2b6e5d8c0a11")]
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/packages.config b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/packages.config
new file mode 100644
index 0000000..8908f05
--- /dev/null
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.Tests/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.sln b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.sln
index 998e209..f244fba 100644
--- a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.sln
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.31105.61
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreateVmSample", "CreateVmSample\CreateVmSample.csproj", "{95737F72-6C25-4AE3-98D2-FC0258A0F612}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreateVmSample.Tests", "CreateVmSample.Tests\CreateVmSample.Tests.csproj", "{7F3C9A21-4E2B-4C7D-9F1A-2B6E5D8C0A11}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{95737F72-6C25-4AE3-98D2-FC0258A0F612}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95737F72-6C25-4AE3-98D2-FC0258A0F612}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95737F72-6C25-4AE3-98D2-FC0258A0F612}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F3C9A21-4E2B-4C7D-9F1A-2B6E5D8C0A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F3C9A21-4E2B-4C7D-9F1A-2B6E5D8C0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F3C9A21-4E2B-4C7D-9F1A-2B6E5D8C0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F3C9A21-4E2B-4C7D-9F1A-2B6E5D8C0A11}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/App.config b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/App.config
index fa062c2..b38a435 100644
--- a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/App.config
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/App.config
@@ -7,7 +7,7 @@
-
+
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/AseServiceClientCredentials.cs b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/AseServiceClientCredentials.cs
index 7e87f3e..75c0798 100644
--- a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/AseServiceClientCredentials.cs
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/AseServiceClientCredentials.cs
@@ -8,6 +8,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -17,6 +18,9 @@ namespace CreateVmSample
public class AseServiceClientCredentials : ServiceClientCredentials
{
private const string PowerShellAppId = "1950a258-227b-4e31-a9cf-717495945fc2";
+ private static readonly object CertificateValidationLock = new object();
+ private static readonly HashSet AllowedCertificateHosts = new HashSet(StringComparer.OrdinalIgnoreCase);
+ private static string ExpectedCertificateThumbprint;
///
/// login client id
@@ -73,26 +77,19 @@ public string AudienceResourceUrl
protected set;
}
- static AseServiceClientCredentials()
- {
- ServicePointManager.ServerCertificateValidationCallback = new
- RemoteCertificateValidationCallback
- (
- delegate { return true; }
- );
- }
-
public AseServiceClientCredentials(string clientId,
string clientSecret,
string tenantId,
string audianceResourceUrl,
- string authenticationAuthority)
+ string authenticationAuthority,
+ string expectedCertificateThumbprint)
{
this.ClientId = clientId;
this.ClientSecretKey = clientSecret;
this.TenantId = tenantId;
this.AudienceResourceUrl = audianceResourceUrl;
this.AuthenticationAuthority = authenticationAuthority;
+ ConfigureCertificateValidation(expectedCertificateThumbprint, audianceResourceUrl, authenticationAuthority);
}
public AseServiceClientCredentials(string clientId,
@@ -104,12 +101,112 @@ public AseServiceClientCredentials(string clientId,
clientSecret,
tenantId,
$"https://management.dbe-{edgeApplianceHostName.ToLower()}.microsoftdatabox.com/",
- $"https://login.dbe-{edgeApplianceHostName.ToLower()}.microsoftdatabox.com/adfs/"
+ $"https://login.dbe-{edgeApplianceHostName.ToLower()}.microsoftdatabox.com/adfs/",
+ null
+ )
+ {
+
+ }
+
+ public AseServiceClientCredentials(string clientId,
+ string clientSecret,
+ string tenantId,
+ string edgeApplianceHostName,
+ string expectedCertificateThumbprint)
+ : this(
+ clientId,
+ clientSecret,
+ tenantId,
+ $"https://management.dbe-{edgeApplianceHostName.ToLower()}.microsoftdatabox.com/",
+ $"https://login.dbe-{edgeApplianceHostName.ToLower()}.microsoftdatabox.com/adfs/",
+ expectedCertificateThumbprint
)
{
}
+ internal static void ConfigureCertificateValidationForTest(string expectedCertificateThumbprint, string audienceResourceUrl, string authenticationAuthority)
+ {
+ ConfigureCertificateValidation(expectedCertificateThumbprint, audienceResourceUrl, authenticationAuthority);
+ }
+
+ internal static void ResetCertificateValidationForTest()
+ {
+ lock (CertificateValidationLock)
+ {
+ AllowedCertificateHosts.Clear();
+ ExpectedCertificateThumbprint = null;
+ ServicePointManager.ServerCertificateValidationCallback = null;
+ }
+ }
+
+ private static void ConfigureCertificateValidation(string expectedCertificateThumbprint, string audienceResourceUrl, string authenticationAuthority)
+ {
+ var normalizedThumbprint = NormalizeThumbprint(expectedCertificateThumbprint);
+ if (string.IsNullOrEmpty(normalizedThumbprint))
+ {
+ return;
+ }
+
+ lock (CertificateValidationLock)
+ {
+ ExpectedCertificateThumbprint = normalizedThumbprint;
+ AllowedCertificateHosts.Clear();
+ AllowedCertificateHosts.Add(new Uri(audienceResourceUrl).Host);
+ AllowedCertificateHosts.Add(new Uri(authenticationAuthority).Host);
+ ServicePointManager.ServerCertificateValidationCallback = ValidatePinnedCertificate;
+ }
+ }
+
+ internal static bool ValidatePinnedCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
+ {
+ if (sslPolicyErrors == SslPolicyErrors.None)
+ {
+ return true;
+ }
+
+ if (certificate == null || string.IsNullOrEmpty(ExpectedCertificateThumbprint))
+ {
+ return false;
+ }
+
+ var requestHost = ExtractRequestHost(sender);
+ if (string.IsNullOrEmpty(requestHost) || !AllowedCertificateHosts.Contains(requestHost))
+ {
+ return false;
+ }
+
+ return string.Equals(
+ NormalizeThumbprint(certificate.GetCertHashString()),
+ ExpectedCertificateThumbprint,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string ExtractRequestHost(object sender)
+ {
+ switch (sender)
+ {
+ case HttpWebRequest webRequest:
+ return webRequest.RequestUri?.Host;
+ case HttpRequestMessage requestMessage:
+ return requestMessage.RequestUri?.Host;
+ case string host:
+ return host;
+ default:
+ return null;
+ }
+ }
+
+ internal static string NormalizeThumbprint(string thumbprint)
+ {
+ if (string.IsNullOrWhiteSpace(thumbprint))
+ {
+ return null;
+ }
+
+ return new string(thumbprint.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
+ }
+
public override void InitializeServiceClient(ServiceClient client)
{
var authContext = new AuthenticationContext($"{this.AuthenticationAuthority}", false);
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/CreateVmSample.csproj b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/CreateVmSample.csproj
index f886381..ad2da5c 100644
--- a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/CreateVmSample.csproj
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/CreateVmSample.csproj
@@ -52,8 +52,8 @@
..\packages\Microsoft.Rest.ClientRuntime.Azure.3.3.19\lib\net461\Microsoft.Rest.ClientRuntime.Azure.dll
-
- ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
+
+ ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Program.cs b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Program.cs
index abad8b5..3a17d7c 100644
--- a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Program.cs
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Program.cs
@@ -52,6 +52,11 @@ static void Main(string[] args)
// you can get this value from the DBE local use
var hostName = "";
+ // Optional appliance certificate thumbprint.
+ // Leave this blank to use normal TLS validation.
+ // Set it only when you need to trust a specific ASE certificate for the management and login endpoints.
+ var applianceCertificateThumbprint = "";
+
// Local Arm Authority URL
// This is the authority/authenticating website URL
// You can also find this value from the local UI
@@ -115,8 +120,8 @@ static void Main(string[] args)
// construct a credentials object.
// If you have changed the domain name of the device you below constructor to consruct the credentials object
- // var clientCreds = new AseServiceClientCredentials(userName, password, tenantId, managementEndpointUrl, authenticationAuthorityUrl);
- var clientCreds = new AseServiceClientCredentials(userName, password, tenantId, hostName);
+ // var clientCreds = new AseServiceClientCredentials(userName, password, tenantId, managementEndpointUrl, authenticationAuthorityUrl, applianceCertificateThumbprint);
+ var clientCreds = new AseServiceClientCredentials(userName, password, tenantId, hostName, applianceCertificateThumbprint);
// Once you have created a credentials obejct you can now interact with any Azure SDK component and operate with the device
diff --git a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Properties/AssemblyInfo.cs b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Properties/AssemblyInfo.cs
index 9a52b91..5fe9b3e 100644
--- a/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Properties/AssemblyInfo.cs
+++ b/dotnetSamples/LocalArm/CreateVmSample/CreateVmSample/Properties/AssemblyInfo.cs
@@ -2,6 +2,9 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+// Allow the test project to exercise internal certificate-pinning members.
+[assembly: InternalsVisibleTo("CreateVmSample.Tests")]
+
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.