Skip to content

Commit

Permalink
feat: Handle authenticate certificate policy in emulator (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mielek authored Jan 9, 2025
1 parent a43c630 commit 860ef46
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/Testing/Document/MockAuthenticationCertificateProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Security.Cryptography.X509Certificates;

using Azure.ApiManagement.PolicyToolkit.Authoring;
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;

Expand Down Expand Up @@ -34,5 +36,8 @@ internal Setup(

public void WithCallback(Action<GatewayContext, CertificateAuthenticationConfig> callback) =>
_handler.CallbackSetup.Add((_predicate, callback).ToTuple());

public void WithCertificate(X509Certificate2 certificate) =>
_handler.CertificateSetup.Add((_predicate, certificate).ToTuple());
}
}
4 changes: 4 additions & 0 deletions src/Testing/Document/TestDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Azure.ApiManagement.PolicyToolkit.Authoring;
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;

namespace Azure.ApiManagement.PolicyToolkit.Testing.Document;

Expand All @@ -18,4 +19,7 @@ public static MockPoliciesProvider<IOutboundContext> SetupOutbound(this TestDocu

public static MockPoliciesProvider<IOnErrorContext> SetupOnError(this TestDocument document) =>
new(document.Context.OnErrorProxy);

public static CertificateStore SetupCertificateStore(this TestDocument document) =>
document.Context.CertificateStore;
}
21 changes: 21 additions & 0 deletions src/Testing/Emulator/Data/CertificateStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Security.Cryptography.X509Certificates;

namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;

public class CertificateStore
{
internal readonly Dictionary<string, X509Certificate2> ById = new();
internal readonly Dictionary<string, X509Certificate2> ByThumbprint = new();

public CertificateStore WithCertificateById(string id, X509Certificate2 certificate)
{
ById.Add(id, certificate);
return this;
}

public CertificateStore WithCertificateByThumbprint(string thumbprint, X509Certificate2 certificate)
{
ByThumbprint.Add(thumbprint, certificate);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Security.Cryptography.X509Certificates;

using Azure.ApiManagement.PolicyToolkit.Authoring;

namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;

[Section(nameof(IInboundContext))]
internal class AuthenticationCertificateHandler : PolicyHandler<CertificateAuthenticationConfig>
{
public List<Tuple<
Func<GatewayContext, CertificateAuthenticationConfig, bool>,
X509Certificate2
>> CertificateSetup { get; } = new();

public override string PolicyName => nameof(IInboundContext.AuthenticationCertificate);

protected override void Handle(GatewayContext context, CertificateAuthenticationConfig config)
{
throw new NotImplementedException();
var certificateFromCallback = CertificateSetup.Find(tuple => tuple.Item1(context, config))?.Item2;
if (certificateFromCallback is not null)
{
context.Request.Certificate = certificateFromCallback;
return;
}

var certificateStore = context.CertificateStore;

if (!string.IsNullOrWhiteSpace(config.Thumbprint))
{
context.Request.Certificate = certificateStore.ByThumbprint.GetValueOrDefault(config.Thumbprint);
}
else if (!string.IsNullOrWhiteSpace(config.CertificateId))
{
context.Request.Certificate = certificateStore.ById.GetValueOrDefault(config.CertificateId);
}
else if (config.Body is not null)
{
context.Request.Certificate = new X509Certificate2(config.Body, config.Password);
}
else
{
throw new InvalidOperationException(
"AuthenticationCertificatePolicy doesn't have certificate source defined");
}
}
}
2 changes: 2 additions & 0 deletions src/Testing/GatewayContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Azure.ApiManagement.PolicyToolkit.Authoring;
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator;
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data;
using Azure.ApiManagement.PolicyToolkit.Testing.Expressions;

namespace Azure.ApiManagement.PolicyToolkit.Testing;
Expand All @@ -13,6 +14,7 @@ public class GatewayContext : MockExpressionContext
internal readonly SectionContextProxy<IBackendContext> BackendProxy;
internal readonly SectionContextProxy<IOutboundContext> OutboundProxy;
internal readonly SectionContextProxy<IOnErrorContext> OnErrorProxy;
internal readonly CertificateStore CertificateStore = new();

public GatewayContext()
{
Expand Down
102 changes: 100 additions & 2 deletions test/Test.Testing/Emulator/Policies/AuthenticationCertificateTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

using Azure.ApiManagement.PolicyToolkit.Authoring;
using Azure.ApiManagement.PolicyToolkit.Authoring.Expressions;
using Azure.ApiManagement.PolicyToolkit.Testing;
using Azure.ApiManagement.PolicyToolkit.Testing.Document;

Expand All @@ -10,18 +14,41 @@ namespace Test.Emulator.Emulator.Policies;
[TestClass]
public class AuthenticationCertificateTests
{
class SimpleAuthenticationCertificate : IDocument
class ByIdCertificate : IDocument
{
public void Inbound(IInboundContext context)
{
context.AuthenticationCertificate(new CertificateAuthenticationConfig { CertificateId = "abcdefgh" });
}
}

class ByThumbprintCertificate : IDocument
{
public void Inbound(IInboundContext context)
{
context.AuthenticationCertificate(new CertificateAuthenticationConfig { Thumbprint = "abcdefgh" });
}
}

class ByBodyCertificate : IDocument
{
public void Inbound(IInboundContext context)
{
context.AuthenticationCertificate(
new CertificateAuthenticationConfig
{
Body = GetCertBody(context.ExpressionContext), Password = "testPass"
});
}

public byte[] GetCertBody(IExpressionContext context) =>
context.Deployment.Certificates["someKey"].Export(X509ContentType.Pfx, "testPass");
}

[TestMethod]
public void AuthenticationCertificate_Callback()
{
var test = new SimpleAuthenticationCertificate().AsTestDocument();
var test = new ByIdCertificate().AsTestDocument();
var executedCallback = false;
test.SetupInbound().AuthenticationCertificate().WithCallback((_, _) =>
{
Expand All @@ -32,4 +59,75 @@ public void AuthenticationCertificate_Callback()

executedCallback.Should().BeTrue();
}

[TestMethod]
public void AuthenticationCertificate_ReturnCertificate()
{
var certificate = CreateTestCertificate();
var test = new ByIdCertificate().AsTestDocument();
test.SetupInbound().AuthenticationCertificate().WithCertificate(certificate);

test.RunInbound();

test.Context.Request.Certificate.Should().Be(certificate);
}

[TestMethod]
public void AuthenticationCertificate_SetupCertificateStore_WithCertificateByThumbprint()
{
var certificate = CreateTestCertificate();
var test = new ByThumbprintCertificate().AsTestDocument();
test.SetupCertificateStore().WithCertificateByThumbprint("abcdefgh", certificate);

test.RunInbound();

test.Context.Request.Certificate.Should().Be(certificate);
}

[TestMethod]
public void AuthenticationCertificate_SetupCertificateStore_WithCertificateById()
{
var certificate = CreateTestCertificate();
var test = new ByIdCertificate().AsTestDocument();
test.SetupCertificateStore().WithCertificateById("abcdefgh", certificate);

test.RunInbound();

test.Context.Request.Certificate.Should().Be(certificate);
}

[TestMethod]
public void AuthenticationCertificate_Body()
{
var certificate = CreateTestCertificate();
var test = new ByBodyCertificate().AsTestDocument();
test.Context.Deployment.Certificates.Add("someKey", certificate);

test.RunInbound();

test.Context.Request.Certificate.Should().Be(certificate);
}

public X509Certificate2 CreateTestCertificate()
{
using RSA rsa = RSA.Create(2048);
var request = new CertificateRequest(
"CN=MyCertificate",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);

// Add extensions
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
request.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
request.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));

var certificate = request.CreateSelfSigned(
DateTimeOffset.Now,
DateTimeOffset.Now.AddYears(1));
return certificate;
}
}

0 comments on commit 860ef46

Please sign in to comment.