Add support for self-signed certificates in subscriber

This commit is contained in:
Vidya Kukke 2020-02-02 22:47:09 -08:00
Родитель b35b3d4b53
Коммит b32df18702
24 изменённых файлов: 728 добавлений и 531 удалений

Просмотреть файл

@ -0,0 +1,52 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
{
public static class CertificateHelper
{
public static void ImportCertificate(params X509Certificate2[] certificates)
{
if (certificates != null)
{
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
StoreLocation storeLocation = StoreLocation.CurrentUser;
using (var store = new X509Store(storeName, storeLocation))
{
store.Open(OpenFlags.ReadWrite);
foreach (X509Certificate2 cert in certificates)
{
store.Add(cert);
}
}
}
}
public static bool IsCACertificate(X509Certificate2 certificate)
{
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
// The keyCertSign bit is asserted when the subject public key is
// used for verifying a signature on public key certificates. If the
// keyCertSign bit is asserted, then the cA bit in the basic
// constraints extension (section 4.2.1.10) MUST also be asserted.
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
// The cA boolean indicates whether the certified public key belongs to
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
// the key usage extension MUST NOT be asserted.
X509ExtensionCollection extensionCollection = certificate.Extensions;
foreach (X509Extension extension in extensionCollection)
{
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
{
if (basicConstraintExtension.CertificateAuthority)
{
return true;
}
}
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class CertificateResponse
{
public PrivateKey PrivateKey { get; set; }
public string Certificate { get; set; }
public DateTime? Expiration { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class IdentityCertificateRequest
{
public DateTime Expiration { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class PrivateKey
{
[JsonConverter(typeof(StringEnumConverter))]
public PrivateKeyType? Type { get; set; }
public string Ref { get; set; }
public string Bytes { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public enum PrivateKeyType
{
Ref = 0,
Key = 1,
}
}

Просмотреть файл

@ -0,0 +1,16 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class ServerCertificateRequest
{
public string CommonName { get; set; }
public DateTime Expiration { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,12 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class TrustBundleResponse
{
public string Certificate { get; set; }
}
}

Просмотреть файл

@ -1,63 +0,0 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using Newtonsoft.Json;
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
{
public enum PrivateKeyType
{
[System.Runtime.Serialization.EnumMember(Value = "ref")]
Ref = 0,
[System.Runtime.Serialization.EnumMember(Value = "key")]
Key = 1,
}
public class IdentityCertificateRequest
{
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
public DateTime Expiration { get; set; }
}
public class ServerCertificateRequest
{
[JsonProperty("commonName", Required = Newtonsoft.Json.Required.Always)]
public string CommonName { get; set; }
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
public DateTime Expiration { get; set; }
}
public class CertificateResponse
{
[JsonProperty("privateKey", Required = Newtonsoft.Json.Required.Always)]
public PrivateKey PrivateKey { get; set; }
[JsonProperty("certificate", Required = Newtonsoft.Json.Required.Always)]
public string Certificate { get; set; }
[JsonProperty("expiration", Required = Newtonsoft.Json.Required.Always)]
public DateTime Expiration { get; set; }
}
public class PrivateKey
{
[JsonProperty("type", Required = Required.Always)]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public PrivateKeyType Type { get; set; }
[JsonProperty("ref", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
public string Ref { get; set; }
[JsonProperty("bytes", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
public string Bytes { get; set; }
}
public class TrustBundleResponse
{
[Newtonsoft.Json.JsonProperty("certificate", Required = Newtonsoft.Json.Required.Always)]
public string Certificate { get; set; }
}
}

Просмотреть файл

@ -1,17 +0,0 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
{
internal class IoTEdgeConstants
{
public const string ModuleGenerationId = "IOTEDGE_MODULEGENERATIONID";
public const string ModuleId = "IOTEDGE_MODULEID";
public const string WorkloadUri = "IOTEDGE_WORKLOADURI";
public const string WorkloadApiVersion = "IOTEDGE_APIVERSION";
public const string EdgeGatewayHostName = "IOTEDGE_GATEWAYHOSTNAME";
public const string UnixScheme = "unix";
public const int DefaultServerCertificateValidityInDays = 90;
public const int DefaultIdentityCertificateValidityInDays = 7;
}
}

Просмотреть файл

@ -1,397 +0,0 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
{
public class IoTSecurity
{
public void ImportCertificate(IEnumerable<X509Certificate2> certificates)
{
if (certificates != null)
{
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
StoreLocation storeLocation = StoreLocation.CurrentUser;
using (var store = new X509Store(storeName, storeLocation))
{
store.Open(OpenFlags.ReadWrite);
foreach (X509Certificate2 cert in certificates)
{
store.Add(cert);
}
}
}
}
public async Task<(X509Certificate2, IEnumerable<X509Certificate2>)> GetClientCertificateAsync()
{
Uri workloadUri = this.GetWorkloadUri();
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
Uri workloadRequestUri = this.GetIdentityCertificateRequestUri(workloadUri);
int certificateValidityInDays = IoTEdgeConstants.DefaultIdentityCertificateValidityInDays;
DateTime expirationTime = DateTime.UtcNow.AddDays(certificateValidityInDays);
var identityCertificateRequest = new IdentityCertificateRequest() { Expiration = expirationTime };
var errorMessage = "Failed to retrieve ClientCertificate from IoTEdge Security Daemon.";
try
{
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
{
string requestString = JsonConvert.SerializeObject(identityCertificateRequest);
var content = new StringContent(requestString);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, workloadRequestUri))
{
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequest.Content = content;
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None))
{
if (httpResponse.StatusCode == HttpStatusCode.Created)
{
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responseData);
IEnumerable<string> rawCerts = this.ParseResponse(cr.Certificate);
if (rawCerts.FirstOrDefault() == null)
{
throw new Exception("Did not receive an identity certificate from IoTEdge daemon!");
}
return this.CreateX509Certificates(rawCerts, cr.PrivateKey.Bytes, moduleId);
}
errorMessage = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new Exception(errorMessage);
}
}
}
}
catch (Exception e)
{
throw new Exception($"Failed to retrieve client certificate from IoTEdge Security Daemon. Reason: {e.Message}");
}
}
public async Task<(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain)> GetServerCertificateAsync()
{
Uri workloadUri = this.GetWorkloadUri();
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
Uri workloadRequestUri = this.GetServerCertificateRequestUri(workloadUri);
ServerCertificateRequest scRequest = this.GetServerCertificateRequest(IoTEdgeConstants.DefaultServerCertificateValidityInDays);
try
{
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
{
string scrString = JsonConvert.SerializeObject(scRequest);
var content = new StringContent(scrString);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, workloadRequestUri))
{
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequest.Content = content;
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (httpResponse.StatusCode == HttpStatusCode.Created)
{
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responseData);
IEnumerable<string> rawCerts = this.ParseResponse(cr.Certificate);
if (rawCerts.FirstOrDefault() == null)
{
throw new Exception($"Failed to retrieve serverCertificate from IoTEdge Security daemon. Reason: Security daemon return empty response.");
}
(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) = this.CreateX509Certificates(rawCerts, cr.PrivateKey.Bytes, moduleId);
return (serverCertificate, certificateChain);
}
string errorData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new Exception(errorData);
}
}
}
}
catch (Exception e)
{
throw new Exception($"Failed to retrieve server certificate from IoTEdge Security Daemon. Reason: {e.Message}");
}
}
private Uri GetWorkloadUri() => new Uri(Environment.GetEnvironmentVariable(IoTEdgeConstants.WorkloadUri));
private string GetIoTEdgeEnvironmentVariable(string envVarName) => Environment.GetEnvironmentVariable(envVarName);
private Uri GetIdentityCertificateRequestUri(Uri workloadUri)
{
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
string urlEncodedModuleId = WebUtility.UrlEncode(moduleId);
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
workloadRequestUriBuilder.Append($"/modules/{urlEncodedModuleId}/certificate/identity?api-version={urlEncodedWorkloadApiVersion}");
return new Uri(workloadRequestUriBuilder.ToString());
}
private Uri GetServerCertificateRequestUri(Uri workloadUri)
{
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
string moduleId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleId);
string moduleGenerationId = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.ModuleGenerationId);
string urlEncodedModuleId = WebUtility.UrlEncode(moduleId);
string urlEncodedModuleGenerationId = WebUtility.UrlEncode(moduleGenerationId);
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
workloadRequestUriBuilder.Append($"/modules/{urlEncodedModuleId}/genid/{urlEncodedModuleGenerationId}/certificate/server?api-version={urlEncodedWorkloadApiVersion}");
return new Uri(workloadRequestUriBuilder.ToString());
}
private Uri GetTrustBundleRequestUri(Uri workloadUri)
{
string workloadApiVersion = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.WorkloadApiVersion);
string urlEncodedWorkloadApiVersion = WebUtility.UrlEncode(workloadApiVersion);
string workloadBaseUrl = this.GetBaseUrl(workloadUri).TrimEnd('/');
var workloadRequestUriBuilder = new StringBuilder(workloadBaseUrl);
workloadRequestUriBuilder.Append($"/trust-bundle?api-version={urlEncodedWorkloadApiVersion}");
return new Uri(workloadRequestUriBuilder.ToString());
}
private ServerCertificateRequest GetServerCertificateRequest(int validityInDays = 90)
{
string edgeDeviceHostName = this.GetIoTEdgeEnvironmentVariable(IoTEdgeConstants.EdgeGatewayHostName);
DateTime expirationTime = DateTime.UtcNow.AddDays(validityInDays);
return new ServerCertificateRequest()
{
CommonName = edgeDeviceHostName,
Expiration = expirationTime,
};
}
private string GetBaseUrl(Uri workloadUri)
{
if (workloadUri.Scheme.Equals(IoTEdgeConstants.UnixScheme, StringComparison.OrdinalIgnoreCase))
{
return $"http://{workloadUri.Segments.Last()}";
}
return workloadUri.OriginalString;
}
private HttpClient GetHttpClient(Uri workloadUri)
{
if (workloadUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || workloadUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
return new HttpClient();
}
else if (workloadUri.Scheme.Equals(IoTEdgeConstants.UnixScheme, StringComparison.OrdinalIgnoreCase))
{
return new HttpClient(new HttpUdsMessageHandler(workloadUri));
}
throw new Exception($"Unknow workloadUri schema specified. {workloadUri}");
}
private IList<string> ParseResponse(string certificateChain)
{
if (string.IsNullOrEmpty(certificateChain))
{
throw new InvalidOperationException("Trusted certificates can not be null or empty.");
}
// Extract each certificate's string. The final string from the split will either be empty
// or a non-certificate entry, so it is dropped.
string delimiter = "-----END CERTIFICATE-----";
string[] rawCerts = certificateChain.Split(new[] { delimiter }, StringSplitOptions.None);
return rawCerts.Take(count: rawCerts.Count() - 1).Select(c => $"{c}{delimiter}").ToList();
}
private (X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) CreateX509Certificates(IEnumerable<string> rawCerts, string privateKey, string moduleId)
{
string primaryCert = rawCerts.First();
RsaPrivateCrtKeyParameters keyParams = null;
IEnumerable<X509Certificate2> x509CertsChain = this.ConvertToX509(rawCerts.Skip(1));
IList<X509CertificateEntry> chainCertEntries = new List<X509CertificateEntry>();
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
// note: the seperator between the certificate and private key is added for safety to delinate the cert and key boundary
var sr = new StringReader(primaryCert + "\r\n" + privateKey);
var pemReader = new PemReader(sr);
object certObject = pemReader.ReadObject();
while (certObject != null)
{
if (certObject is Org.BouncyCastle.X509.X509Certificate x509Cert)
{
chainCertEntries.Add(new X509CertificateEntry(x509Cert));
}
// when processing certificates generated via openssl certObject type is of AsymmetricCipherKeyPair
if (certObject is AsymmetricCipherKeyPair)
{
certObject = ((AsymmetricCipherKeyPair)certObject).Private;
}
if (certObject is RsaPrivateCrtKeyParameters)
{
keyParams = (RsaPrivateCrtKeyParameters)certObject;
}
certObject = pemReader.ReadObject();
}
if (keyParams == null)
{
throw new InvalidOperationException("Private key is required");
}
store.SetKeyEntry(moduleId, new AsymmetricKeyEntry(keyParams), chainCertEntries.ToArray());
using (var p12File = new MemoryStream())
{
store.Save(p12File, Array.Empty<char>(), new SecureRandom());
var x509PrimaryCert = new X509Certificate2(p12File.ToArray());
return (x509PrimaryCert, x509CertsChain);
}
}
public async Task<IEnumerable<X509Certificate2>> GetTrustBundleAsync()
{
Uri workloadUri = this.GetWorkloadUri();
using (HttpClient httpClient = this.GetHttpClient(workloadUri))
{
Uri workloadRequestUri = this.GetTrustBundleRequestUri(workloadUri);
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, workloadRequestUri))
{
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using (HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None))
{
if (httpResponse.StatusCode == HttpStatusCode.OK)
{
string responseData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
TrustBundleResponse trustBundleResponse = JsonConvert.DeserializeObject<TrustBundleResponse>(responseData);
IEnumerable<string> rawCerts = this.ParseResponse(trustBundleResponse.Certificate);
if (rawCerts.FirstOrDefault() == null)
{
throw new Exception($"Failed to retrieve trustbundle from security daemon.");
}
return this.ConvertToX509(rawCerts);
}
string errorData = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new Exception($"Failed to retrieve trustbundle from security daemon. Reason: {errorData}");
}
}
}
}
public async Task ValidateClientCertificateAsync(X509Certificate2 clientCertificate)
{
// Please add validation more validations as appropriate
if (this.IsCACertificate(clientCertificate))
{
throw new Exception("Cannot use CA certificate for client authentication!");
}
IEnumerable<X509Certificate2> trustedCertificates = await this.GetTrustBundleAsync();
using (X509Chain chain = new X509Chain())
{
foreach (X509Certificate2 trustedClientCert in trustedCertificates)
{
chain.ChainPolicy.ExtraStore.Add(trustedClientCert);
}
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// IoTEdge generates a self-signed certificate by default, that is not rooted in a root certificate that is trusted by the trust provider hence this flag is needed
// so that build returns true if root terminates in a self-signed certificate
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
}
if (!chain.Build(clientCertificate))
{
var errorMessageBuilder = new StringBuilder();
foreach (X509ChainStatus cs in chain.ChainStatus)
{
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
errorMessageBuilder.AppendLine();
}
throw new Exception($"ClientCertificate is not valid! Reason: Failed chain validation. Details: {errorMessageBuilder}");
}
}
}
private X509Certificate2[] ConvertToX509(IEnumerable<string> rawCerts)
{
return rawCerts
.Select(c => Encoding.UTF8.GetBytes(c))
.Select(c => new X509Certificate2(c))
.ToArray();
}
private bool IsCACertificate(X509Certificate2 certificate)
{
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
// The keyCertSign bit is asserted when the subject public key is
// used for verifying a signature on public key certificates. If the
// keyCertSign bit is asserted, then the cA bit in the basic
// constraints extension (section 4.2.1.10) MUST also be asserted.
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
// The cA boolean indicates whether the certified public key belongs to
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
// the key usage extension MUST NOT be asserted.
X509ExtensionCollection extensionCollection = certificate.Extensions;
foreach (X509Extension extension in extensionCollection)
{
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
{
if (basicConstraintExtension.CertificateAuthority)
{
return true;
}
}
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,287 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
[Obsolete("This is a pubternal API that's being made public as a stop-gap measure. It will be removed from the Event Grid SDK nuget package as soon IoT Edge SDK ships with a built-in a security daemon client.")]
public sealed class SecurityDaemonClient : IDisposable
{
private const string UnixScheme = "unix";
private const int DefaultServerCertificateValidityInDays = 90;
private const int DefaultIdentityCertificateValidityInDays = 7;
private readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
FloatParseHandling = FloatParseHandling.Decimal,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new JsonConverter[] { new StringEnumConverter() },
};
private readonly string moduleGenerationId;
private readonly string edgeGatewayHostName;
private readonly string workloadApiVersion;
private readonly HttpClient httpClient;
private readonly Uri getTrustBundleUri;
private readonly Uri postIdentityCertificateRequestUri;
private readonly Uri postServerCertificateRequestUri;
private readonly string asString;
public SecurityDaemonClient()
{
this.ModuleId = Environment.GetEnvironmentVariable("IOTEDGE_MODULEID");
this.DeviceId = Environment.GetEnvironmentVariable("IOTEDGE_DEVICEID");
string iotHubHostName = Environment.GetEnvironmentVariable("IOTEDGE_IOTHUBHOSTNAME");
this.IotHubName = iotHubHostName.Split('.').FirstOrDefault();
this.moduleGenerationId = Environment.GetEnvironmentVariable("IOTEDGE_MODULEGENERATIONID");
this.edgeGatewayHostName = Environment.GetEnvironmentVariable("IOTEDGE_GATEWAYHOSTNAME");
this.workloadApiVersion = Environment.GetEnvironmentVariable("IOTEDGE_APIVERSION");
string workloadUriString = Environment.GetEnvironmentVariable("IOTEDGE_WORKLOADURI");
Validate.ArgumentNotNullOrEmpty(this.ModuleId, nameof(this.ModuleId));
Validate.ArgumentNotNullOrEmpty(this.DeviceId, nameof(this.DeviceId));
Validate.ArgumentNotNullOrEmpty(this.IotHubName, nameof(this.IotHubName));
Validate.ArgumentNotNullOrEmpty(this.moduleGenerationId, nameof(this.moduleGenerationId));
Validate.ArgumentNotNullOrEmpty(this.edgeGatewayHostName, nameof(this.edgeGatewayHostName));
Validate.ArgumentNotNullOrEmpty(this.workloadApiVersion, nameof(this.workloadApiVersion));
Validate.ArgumentNotNullOrEmpty(workloadUriString, nameof(workloadUriString));
var workloadUri = new Uri(workloadUriString);
string baseUrlForRequests;
if (workloadUri.Scheme.Equals(SecurityDaemonClient.UnixScheme, StringComparison.OrdinalIgnoreCase))
{
baseUrlForRequests = $"http://{workloadUri.Segments.Last()}";
this.httpClient = new HttpClient(new HttpUdsMessageHandler(workloadUri));
}
else if (workloadUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
workloadUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
baseUrlForRequests = workloadUriString;
this.httpClient = new HttpClient();
}
else
{
throw new InvalidOperationException($"Unknown workloadUri scheme specified. {workloadUri}");
}
baseUrlForRequests = baseUrlForRequests.TrimEnd();
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
string encodedApiVersion = UrlEncoder.Default.Encode(this.workloadApiVersion);
string encodedModuleId = UrlEncoder.Default.Encode(this.ModuleId);
string encodedModuleGenerationId = UrlEncoder.Default.Encode(this.moduleGenerationId);
this.getTrustBundleUri = new Uri($"{baseUrlForRequests}/trust-bundle?api-version={encodedApiVersion}");
this.postIdentityCertificateRequestUri = new Uri($"{baseUrlForRequests}/modules/{encodedModuleId}/certificate/identity?api-version={encodedApiVersion}");
this.postServerCertificateRequestUri = new Uri($"{baseUrlForRequests}/modules/{encodedModuleId}/genid/{encodedModuleGenerationId}/certificate/server?api-version={encodedApiVersion}");
var settings = new
{
this.ModuleId,
this.DeviceId,
IotHubHostName = iotHubHostName,
ModuleGenerationId = this.moduleGenerationId,
GatewayHostName = this.edgeGatewayHostName,
WorkloadUri = workloadUriString,
WorkloadApiVersion = this.workloadApiVersion,
};
this.asString = $"{nameof(SecurityDaemonClient)}{JsonConvert.SerializeObject(settings, Formatting.None, this.jsonSettings)}";
}
public string IotHubName { get; }
public string DeviceId { get; }
public string ModuleId { get; }
public void Dispose() => this.httpClient.Dispose();
public override string ToString() => this.asString;
public Task<(X509Certificate2 serverCert, X509Certificate2[] certChain)> GetServerCertificateAsync(CancellationToken token = default)
{
return this.GetServerCertificateAsync(TimeSpan.FromDays(SecurityDaemonClient.DefaultServerCertificateValidityInDays), token);
}
public async Task<(X509Certificate2 serverCert, X509Certificate2[] certChain)> GetServerCertificateAsync(TimeSpan validity, CancellationToken token = default)
{
var request = new ServerCertificateRequest
{
CommonName = this.edgeGatewayHostName,
Expiration = DateTime.UtcNow.Add(validity),
};
string requestString = JsonConvert.SerializeObject(request, Formatting.None, this.jsonSettings);
using (var content = new StringContent(requestString, Encoding.UTF8, "application/json"))
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, this.postServerCertificateRequestUri) { Content = content })
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
{
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
if (httpResponse.StatusCode == HttpStatusCode.Created)
{
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responsePayload, this.jsonSettings);
return this.CreateX509Certificates(cr);
}
throw new InvalidOperationException($"Failed to retrieve server certificate from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' Request={requestString} This={this}");
}
}
public Task<(X509Certificate2 identityCert, X509Certificate2[] certChain)> GetIdentityCertificateAsync(CancellationToken token = default)
{
return this.GetIdentityCertificateAsync(TimeSpan.FromDays(SecurityDaemonClient.DefaultIdentityCertificateValidityInDays), token);
}
public async Task<(X509Certificate2 identityCert, X509Certificate2[] certChain)> GetIdentityCertificateAsync(TimeSpan validity, CancellationToken token = default)
{
var request = new IdentityCertificateRequest
{
Expiration = DateTime.UtcNow.Add(validity),
};
string requestString = JsonConvert.SerializeObject(request, Formatting.None, this.jsonSettings);
using (var content = new StringContent(requestString, Encoding.UTF8, "application/json"))
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, this.postIdentityCertificateRequestUri) { Content = content })
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
{
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
if (httpResponse.StatusCode == HttpStatusCode.Created)
{
CertificateResponse cr = JsonConvert.DeserializeObject<CertificateResponse>(responsePayload, this.jsonSettings);
return this.CreateX509Certificates(cr);
}
throw new InvalidOperationException($"Failed to retrieve identity certificate from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' Request={requestString} This={this}");
}
}
public async Task<X509Certificate2[]> GetTrustBundleAsync(CancellationToken token = default)
{
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, this.getTrustBundleUri))
using (HttpResponseMessage httpResponse = await this.httpClient.SendAsync(httpRequest, token))
{
string responsePayload = await httpResponse.Content.ReadAsStringAsync();
if (httpResponse.StatusCode == HttpStatusCode.OK)
{
TrustBundleResponse trustBundleResponse = JsonConvert.DeserializeObject<TrustBundleResponse>(responsePayload, this.jsonSettings);
Validate.ArgumentNotNullOrEmpty(trustBundleResponse.Certificate, nameof(trustBundleResponse.Certificate));
string[] rawCerts = ParseCertificateResponse(trustBundleResponse.Certificate);
if (rawCerts.FirstOrDefault() == null)
{
throw new InvalidOperationException($"Failed to retrieve the certificate trust bundle from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' Reason='Security daemon returned an empty response' This={this}");
}
return ConvertToX509(rawCerts);
}
throw new InvalidOperationException($"Failed to retrieve the certificate trust bundle from IoTEdge security daemon. StatusCode={httpResponse.StatusCode} ReasonPhrase='{httpResponse.ReasonPhrase}' ResponsePayload='{responsePayload}' This={this}");
}
}
private static X509Certificate2[] ConvertToX509(IEnumerable<string> rawCerts) => rawCerts.Select(c => new X509Certificate2(Encoding.UTF8.GetBytes(c))).ToArray();
private static string[] ParseCertificateResponse(string certificateChain, [CallerMemberName] string callerMemberName = default)
{
if (string.IsNullOrEmpty(certificateChain))
{
throw new InvalidOperationException($"Trusted certificates can not be null or empty for {callerMemberName}.");
}
// Extract each certificate's string. The final string from the split will either be empty
// or a non-certificate entry, so it is dropped.
string delimiter = "-----END CERTIFICATE-----";
string[] rawCerts = certificateChain.Split(new[] { delimiter }, StringSplitOptions.None);
return rawCerts.Take(count: rawCerts.Length - 1).Select(c => $"{c}{delimiter}").ToArray();
}
private (X509Certificate2 primaryCert, X509Certificate2[] certChain) CreateX509Certificates(CertificateResponse cr, [CallerMemberName] string callerMemberName = default)
{
Validate.ArgumentNotNullOrEmpty(cr.Certificate, nameof(cr.Certificate));
Validate.ArgumentNotNull(cr.Expiration, nameof(cr.Expiration));
Validate.ArgumentNotNull(cr.PrivateKey, nameof(cr.PrivateKey));
Validate.ArgumentNotNull(cr.PrivateKey.Type, nameof(cr.PrivateKey.Type));
Validate.ArgumentNotNull(cr.PrivateKey.Bytes, nameof(cr.PrivateKey.Bytes));
string[] rawCerts = ParseCertificateResponse(cr.Certificate);
if (rawCerts.Length == 0 ||
string.IsNullOrWhiteSpace(rawCerts[0]))
{
throw new InvalidOperationException($"Failed to retrieve certificate from IoTEdge Security daemon for {callerMemberName}. Reason: Security daemon returned an empty response.");
}
string primaryCert = rawCerts[0];
X509Certificate2[] certChain = ConvertToX509(rawCerts.Skip(1));
RsaPrivateCrtKeyParameters keyParams = null;
var chainCertEntries = new List<X509CertificateEntry>();
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
// note: the seperator between the certificate and private key is added for safety to delineate the cert and key boundary
using (var sr = new StringReader(primaryCert + "\r\n" + cr.PrivateKey.Bytes))
{
var pemReader = new PemReader(sr);
object certObject;
while ((certObject = pemReader.ReadObject()) != null)
{
if (certObject is Org.BouncyCastle.X509.X509Certificate x509Cert)
{
chainCertEntries.Add(new X509CertificateEntry(x509Cert));
}
// when processing certificates generated via openssl certObject type is of AsymmetricCipherKeyPair
if (certObject is AsymmetricCipherKeyPair ackp)
{
certObject = ackp.Private;
}
if (certObject is RsaPrivateCrtKeyParameters rpckp)
{
keyParams = rpckp;
}
}
}
if (keyParams == null)
{
throw new InvalidOperationException($"Private key was not found for {callerMemberName}");
}
store.SetKeyEntry(this.ModuleId, new AsymmetricKeyEntry(keyParams), chainCertEntries.ToArray());
using (var ms = new MemoryStream())
{
store.Save(ms, Array.Empty<char>(), new SecureRandom());
var x509PrimaryCert = new X509Certificate2(ms.ToArray());
return (x509PrimaryCert, certChain);
}
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
[Obsolete("This is a pubternal API that's being made public as a stop-gap measure. It will be removed from the Event Grid SDK nuget package as soon IoT Edge SDK ships with a built-in a security daemon client.")]
public class SecurityDaemonHttpClientFactory : IHttpClientFactory
{
private readonly Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> callback;
public SecurityDaemonHttpClientFactory(X509Certificate2 identityCertificate)
: this(identityCertificate, ServiceCertificateValidationCallback)
{
}
public SecurityDaemonHttpClientFactory(X509Certificate2 identityCertificate, Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCallback)
{
this.IdentityCertificate = identityCertificate;
this.callback = serverCertificateCallback;
}
public X509Certificate2 IdentityCertificate { get; }
public static async Task<SecurityDaemonHttpClientFactory> CreateAsync(CancellationToken token = default)
{
using (var iotEdgeClient = new SecurityDaemonClient())
{
(X509Certificate2 identityCertificate, _) = await iotEdgeClient.GetIdentityCertificateAsync(token);
return new SecurityDaemonHttpClientFactory(identityCertificate);
}
}
[SuppressMessage("Microsoft.Reliability", "CA2000: DisposeObjectsBeforeLosingScope", Justification = "The HttpClient owns the lifetime of the handler")]
public HttpClient CreateClient(string name)
{
var httpClientHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = this.callback };
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
httpClientHandler.ClientCertificates.Add(this.IdentityCertificate);
return new HttpClient(httpClientHandler, disposeHandler: true);
}
private static bool ServiceCertificateValidationCallback(HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors errors)
{
return true;
}
}
}

Просмотреть файл

@ -1,14 +1,16 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class HttpBufferedStream : Stream
internal class HttpBufferedStream : Stream
{
private const char CR = '\r';
private const char LF = '\n';

Просмотреть файл

@ -1,5 +1,7 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
@ -9,21 +11,24 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
public class HttpSerializer
internal class HttpRequestResponseSerializer
{
private const char SP = ' ';
private const char CR = '\r';
private const char LF = '\n';
private const char ProtocolVersionSeparator = '/';
private const string Protocol = "HTTP";
private const char HeaderSeparator = ':';
private const string HeaderSeparator = ":";
private const string ContentLengthHeaderName = "content-length";
public byte[] SerializeRequest(HttpRequestMessage request)
{
this.PreProcessRequest(request);
Validate.ArgumentNotNull(request, nameof(request));
Validate.ArgumentNotNull(request.RequestUri, nameof(request.RequestUri));
PreProcessRequest(request);
var builder = new StringBuilder();
// request-line = method SP request-target SP HTTP-version CRLF
@ -60,14 +65,12 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
public async Task<HttpResponseMessage> DeserializeResponseAsync(HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
{
var httpResponse = new HttpResponseMessage();
await this.SetResponseStatusLineAsync(httpResponse, bufferedStream, cancellationToken);
await this.SetHeadersAndContentAsync(httpResponse, bufferedStream, cancellationToken);
await SetResponseStatusLineAsync(httpResponse, bufferedStream, cancellationToken);
await SetHeadersAndContentAsync(httpResponse, bufferedStream, cancellationToken);
return httpResponse;
}
private async Task SetHeadersAndContentAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
private static async Task SetHeadersAndContentAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
{
IList<string> headers = new List<string>();
string line = await bufferedStream.ReadLineAsync(cancellationToken);
@ -113,7 +116,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
}
}
private async Task SetResponseStatusLineAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
private static async Task SetResponseStatusLineAsync(HttpResponseMessage httpResponse, HttpBufferedStream bufferedStream, CancellationToken cancellationToken)
{
string statusLine = await bufferedStream.ReadLineAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(statusLine))
@ -144,7 +147,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
httpResponse.ReasonPhrase = statusLineParts[2];
}
private void PreProcessRequest(HttpRequestMessage request)
private static void PreProcessRequest(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(request.Headers.Host))
{

Просмотреть файл

@ -1,5 +1,7 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
using System.Net.Http;
@ -7,27 +9,30 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Azure.EventGridEdge.Samples.Common.Auth
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
/// <summary>
/// Unix domain message handler.
/// </summary>
public class HttpUdsMessageHandler : HttpMessageHandler
internal class HttpUdsMessageHandler : HttpMessageHandler
{
private readonly Uri providerUri;
public HttpUdsMessageHandler(Uri providerUri)
{
Validate.ArgumentNotNull(providerUri, nameof(providerUri));
this.providerUri = providerUri;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Validate.ArgumentNotNull(request, nameof(request));
using (Socket socket = await this.GetConnectedSocketAsync())
{
using (var stream = new HttpBufferedStream(new NetworkStream(socket, true)))
{
var serializer = new HttpSerializer();
var serializer = new HttpRequestResponseSerializer();
byte[] requestBytes = serializer.SerializeRequest(request);
await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken);

32
Common/auth/Validate.cs Normal file
Просмотреть файл

@ -0,0 +1,32 @@
// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// -----------------------------------------------------------------------
using System;
namespace Microsoft.Azure.EventGridEdge.IotEdge
{
internal static class Validate
{
public static void ArgumentNotNull(object value, string paramName)
{
if (value == null)
{
throw new ArgumentNullException(paramName, $"The argument {paramName} is null.");
}
}
public static void ArgumentNotNullOrEmpty(string value, string paramName)
{
if (value == null)
{
throw new ArgumentNullException(paramName, $"The argument {paramName} is null.");
}
else if (value.Length == 0)
{
throw new ArgumentException(paramName, $"The argument {paramName} is empty.");
}
}
}
}

Просмотреть файл

@ -13,7 +13,7 @@ using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Linq;
using System.Collections.Generic;
using Microsoft.Azure.EventGridEdge.Samples.Common.Auth;
using Microsoft.Azure.EventGridEdge.IotEdge;
namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
{
@ -164,8 +164,8 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Publisher
if (gridConfig.ClientAuth.Source.Equals("IoTEdge", StringComparison.OrdinalIgnoreCase))
{
IoTSecurity iotSecurity = new IoTSecurity();
(X509Certificate2 identityCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetClientCertificateAsync();
SecurityDaemonClient iotSecurity = new SecurityDaemonClient();
(X509Certificate2 identityCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetIdentityCertificateAsync();
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain.First(), identityCertificate));
}
else if (gridConfig.ClientAuth.Source.Equals("BearerToken", StringComparison.OrdinalIgnoreCase))

Просмотреть файл

@ -18,8 +18,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Common\auth\Auth.csproj" />
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
<ProjectReference Include="..\..\..\SecurityDaemonClient\SecurityDaemonClient.csproj" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -0,0 +1,74 @@
// Copyright(c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
{
public static class CertificateHelper
{
[SuppressMessage("Microsoft.Security", "CA5381: DoNotInstallRootCert", Justification = "We're in a docker container, there is no risk to the host machine.")]
public static void ImportIntermediateCAs(params X509Certificate2[] certificates)
{
if (certificates != null)
{
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.CertificateAuthority : StoreName.Root;
StoreLocation storeLocation = StoreLocation.CurrentUser;
using (var store = new X509Store(storeName, storeLocation))
{
Console.WriteLine($"Importing certificate to StoreName:{storeName}, StoreLocation:{storeLocation}");
store.Open(OpenFlags.ReadWrite);
foreach (X509Certificate2 cert in certificates)
{
store.Add(cert);
}
}
}
}
public static void ImportCertificate(X509Certificate2 certificate)
{
if (certificate != null)
{
StoreName storeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StoreName.My : StoreName.Root;
StoreLocation storeLocation = StoreLocation.CurrentUser;
using (var store = new X509Store(storeName, storeLocation))
{
Console.WriteLine($"Importing certificate to StoreName:{storeName}, StoreLocation:{storeLocation}");
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
}
}
}
public static bool IsCACertificate(X509Certificate2 certificate)
{
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
// The keyCertSign bit is asserted when the subject public key is
// used for verifying a signature on public key certificates. If the
// keyCertSign bit is asserted, then the cA bit in the basic
// constraints extension (section 4.2.1.10) MUST also be asserted.
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
// The cA boolean indicates whether the certified public key belongs to
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
// the key usage extension MUST NOT be asserted.
X509ExtensionCollection extensionCollection = certificate.Extensions;
foreach (X509Extension extension in extensionCollection)
{
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
{
if (basicConstraintExtension.CertificateAuthority)
{
return true;
}
}
}
return false;
}
}
}

Просмотреть файл

@ -2,7 +2,6 @@
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
@ -12,8 +11,7 @@ using System.Threading.Tasks;
using Microsoft.Azure.EventGridEdge.SDK;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Linq;
using Microsoft.Azure.EventGridEdge.Samples.Common.Auth;
using Microsoft.Azure.EventGridEdge.IotEdge;
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
{
@ -85,18 +83,24 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
private static async Task<SubscriberHost> SetupSubscriberHostAsync(CancellationTokenSource lifetimeCts)
{
IoTSecurity iotSecurity = new IoTSecurity();
using var securityDaemonClient = new SecurityDaemonClient();
// get server certificate to configure with
(X509Certificate2 serverCertificate, IEnumerable<X509Certificate2> certificateChain) =
await iotSecurity.GetServerCertificateAsync().ConfigureAwait(false);
iotSecurity.ImportCertificate(new List<X509Certificate2>() { serverCertificate });
iotSecurity.ImportCertificate(certificateChain);
Console.WriteLine($"Configure server certificate");
(X509Certificate2 serverCert, X509Certificate2[] certChain) =
await securityDaemonClient.GetServerCertificateAsync().ConfigureAwait(false);
Console.WriteLine($"Server Certificate issue is valid from {serverCertificate.NotBefore}, {serverCertificate.NotAfter}");
CertificateHelper.ImportCertificate(serverCert);
CertificateHelper.ImportIntermediateCAs(serverCert);
CertificateHelper.ImportIntermediateCAs(certChain);
// Configure client trust bundle
Console.WriteLine($"Configure client trust bundle");
var trustBundle = await securityDaemonClient.GetTrustBundleAsync();
CertificateHelper.ImportIntermediateCAs(trustBundle);
// start subscriber webhost
SubscriberHost host = new SubscriberHost(serverCertificate, lifetimeCts);
SubscriberHost host = new SubscriberHost(serverCert, lifetimeCts);
await host.StartAsync().ConfigureAwait(false);
return host;
}
@ -140,10 +144,10 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
private static async Task<EventGridEdgeClient> GetEventGridClientAsync(GridConfiguration gridConfig)
{
IoTSecurity iotSecurity = new IoTSecurity();
using var securityDaemonClient = new SecurityDaemonClient();
// get the client certificate to use when communicating with eventgrid
(X509Certificate2 clientCertificate, IEnumerable<X509Certificate2> chain) = await iotSecurity.GetClientCertificateAsync().ConfigureAwait(false);
(X509Certificate2 clientCertificate, X509Certificate2[] chain) = await securityDaemonClient.GetIdentityCertificateAsync().ConfigureAwait(false);
Console.WriteLine($"Client Certificate issue is valid from {clientCertificate.NotBefore}, {clientCertificate.NotAfter}");
string[] urlTokens = gridConfig.Url.Split(":");
if (urlTokens.Length != 3)
@ -154,7 +158,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
string baseUrl = urlTokens[0] + ":" + urlTokens[1];
int port = int.Parse(urlTokens[2], CultureInfo.InvariantCulture);
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain.First(), clientCertificate));
return new EventGridEdgeClient(baseUrl, port, new CustomHttpClientFactory(chain[0], clientCertificate));
}
private static void LogAndBackoff(string topicName, Exception e)

Просмотреть файл

@ -16,8 +16,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Common\auth\Auth.csproj" />
<ProjectReference Include="..\..\..\SDK\SDK.csproj" />
<ProjectReference Include="..\..\..\SecurityDaemonClient\SecurityDaemonClient.csproj" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Hosting;
@ -35,12 +34,14 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// this is needed because IoTEdge generates a self signed certificate that is not rooted in a root certificate that is trusted by the trust provider.
// Kestrel rejects the request automatically because of this. We return true here so that client validation can happen when routing requests.
o.AllowAnyClientCertificate();
o.CheckCertificateRevocation = false;
o.ClientCertificateValidation = (X509Certificate2 arg1, X509Chain arg2, SslPolicyErrors arg3) =>
// this is needed because IoTEdge generates a self signed certificate that is not rooted in a root certificate that is trusted by the trust provider.
// Kestrel rejects the request automatically because of this. We return true here so that client validation can happen when routing requests.
o.ClientCertificateValidation = (cert, chain, errors) =>
{
Console.WriteLine("ClientCertValidation invoked");
return true;
};
}

Просмотреть файл

@ -2,13 +2,17 @@
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.EventGridEdge.Samples.Common.Auth;
using Microsoft.Azure.EventGridEdge.IotEdge;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
@ -16,7 +20,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
public class HostStartup : IStartup
{
private readonly EventsHandler eventsHandler = new EventsHandler();
private readonly IoTSecurity iotSecurity = new IoTSecurity();
private readonly SecurityDaemonClient iotSecurity = new SecurityDaemonClient();
public void Configure(IApplicationBuilder app)
{
@ -44,7 +48,7 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
}
Console.WriteLine("Validating client certificate...");
await iotSecurity.ValidateClientCertificateAsync(clientCert);
await this.ValidateClientCertificateAsync(clientCert);
}
// TODO: Verify it is eventgrid instance indeed!
@ -56,5 +60,72 @@ namespace Microsoft.Azure.EventGridEdge.Samples.Subscriber
await next().ConfigureAwait(false);
}
}
private async Task ValidateClientCertificateAsync(X509Certificate2 clientCertificate)
{
// Please add validation more validations as appropriate
if (this.IsCACertificate(clientCertificate))
{
throw new Exception("Cannot use CA certificate for client authentication!");
}
IEnumerable<X509Certificate2> trustedCertificates = await this.iotSecurity.GetTrustBundleAsync();
using (X509Chain chain = new X509Chain())
{
foreach (X509Certificate2 trustedClientCert in trustedCertificates)
{
chain.ChainPolicy.ExtraStore.Add(trustedClientCert);
}
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// IoTEdge generates a self-signed certificate by default, that is not rooted in a root certificate that is trusted by the trust provider hence this flag is needed
// so that build returns true if root terminates in a self-signed certificate
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
}
if (!chain.Build(clientCertificate))
{
var errorMessageBuilder = new StringBuilder();
foreach (X509ChainStatus cs in chain.ChainStatus)
{
errorMessageBuilder.AppendFormat(CultureInfo.InvariantCulture, $"ChainStatus: {cs.Status}, ChainStatusInfo: {cs.StatusInformation}");
errorMessageBuilder.AppendLine();
}
throw new Exception($"ClientCertificate is not valid! Reason: Failed chain validation. Details: {errorMessageBuilder}");
}
}
}
private bool IsCACertificate(X509Certificate2 certificate)
{
// https://tools.ietf.org/html/rfc3280#section-4.2.1.3
// The keyCertSign bit is asserted when the subject public key is
// used for verifying a signature on public key certificates. If the
// keyCertSign bit is asserted, then the cA bit in the basic
// constraints extension (section 4.2.1.10) MUST also be asserted.
// https://tools.ietf.org/html/rfc3280#section-4.2.1.10
// The cA boolean indicates whether the certified public key belongs to
// a CA. If the cA boolean is not asserted, then the keyCertSign bit in
// the key usage extension MUST NOT be asserted.
X509ExtensionCollection extensionCollection = certificate.Extensions;
foreach (X509Extension extension in extensionCollection)
{
if (extension is X509BasicConstraintsExtension basicConstraintExtension)
{
if (basicConstraintExtension.CertificateAuthority)
{
return true;
}
}
}
return false;
}
}
}

Просмотреть файл

@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecurityDaemonClient", "Sec
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{34DA2417-E843-402A-BB71-D8385A8735F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Auth", "Common\auth\Auth.csproj", "{4F4E7F1E-4488-4426-99D3-81B37310E0FA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -55,10 +53,6 @@ Global
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8}.Release|Any CPU.Build.0 = Release|Any CPU
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F4E7F1E-4488-4426-99D3-81B37310E0FA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -70,7 +64,6 @@ Global
{3814D171-75B4-4324-8478-DBCCC9CBB5E3} = {1A29FBDF-CDBA-4BD0-9FE5-D40D95FF988D}
{83EC7C9C-AB4E-480C-BC14-1BF688EAA28A} = {AEE9042C-0BC5-4302-9582-9AFA340EF555}
{00E8B3CB-A051-4A13-B965-BE4836E1AFD8} = {AEE9042C-0BC5-4302-9582-9AFA340EF555}
{4F4E7F1E-4488-4426-99D3-81B37310E0FA} = {34DA2417-E843-402A-BB71-D8385A8735F0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {475D2ECB-88BD-47E1-B351-D17F2ACF95B8}