Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cscglobal-caplugin.MockData/CSCGlobalCAPlugin.MockData.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Keyfactor.Extensions.CAPlugin.CSCGlobal.MockData</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\cscglobal-caplugin\CSCGlobalCAPlugin.csproj" />
</ItemGroup>

</Project>
144 changes: 144 additions & 0 deletions cscglobal-caplugin.MockData/MidCycleReissueFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2021 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models;

namespace Keyfactor.Extensions.CAPlugin.CSCGlobal.MockData;

/// <summary>
/// Mock CSC "mid-cycle reissue" API responses, reproducing a real-world scenario: CSC
/// silently reissues a certificate partway through its order term - same uuid, re-signed
/// with the *same* key pair as the original (new serial number, shifted validity dates),
/// and isLifeCycleReIssueCertificate set to true on the next list/get response.
///
/// As of this fixture, the plugin does not special-case this flag anywhere - these fixtures
/// exist to let that gap be exercised and tested before any handling is decided/implemented.
///
/// Metadata (uuid, commonName, certificateType, dates) is taken from real CSC API responses;
/// only the certificate material is synthetic (freshly generated per test run), though the
/// baseline and its reissue deliberately share one RSA key pair, re-signed twice.
/// </summary>
public static class MidCycleReissueFixtures
{
// Keyed by scenario uuid so the baseline and its mid-cycle reissue are re-signed from the
// same key pair, as they would be if CSC reissued the certificate without a new CSR.
private static readonly Dictionary<string, RSA> ScenarioKeys = new();

private static RSA GetOrCreateKey(string uuid)
{
if (!ScenarioKeys.TryGetValue(uuid, out var key))
{
key = RSA.Create(2048);
ScenarioKeys[uuid] = key;
}
return key;
}

private static string MakeApiCertificate(string commonName, RSA key, DateTimeOffset notBefore, DateTimeOffset notAfter)
{
var req = new CertificateRequest($"CN={commonName}", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
using var cert = req.CreateSelfSigned(notBefore, notAfter);
var pem = "-----BEGIN CERTIFICATE-----\n" +
Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks) +
"\n-----END CERTIFICATE-----\n";
return Convert.ToBase64String(Encoding.UTF8.GetBytes(pem));
}

/// <summary>
/// Single-name DV certificate (cnn.keyfactortestb.com). Real uuid/metadata from a CSC
/// sample response; standard mid-order-term sync, no reissue involved.
/// </summary>
public static CertificateResponse SingleNameBaseline()
{
const string uuid = "96d8802a-94f5-4616-a27d-bd51bb6eca18";
var key = GetOrCreateKey(uuid);
var apiCert = MakeApiCertificate("cnn.keyfactortestb.com", key,
DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365));
return new CertificateResponse
{
Uuid = uuid,
CommonName = "cnn.keyfactortestb.com",
AdditionalNames = new List<string>(),
CertificateType = "CSC TrustedSecure DV",
Status = "ACTIVE",
EffectiveDate = "21-Sep-2026",
ExpirationDate = "08-Apr-2027",
OrderValidityStartDate = "21-Sep-2026",
OrderValidityEndDate = "21-Sep-2027",
IsRenewed = false,
IsLifeCycleReIssueCertificate = false,
BusinessUnit = "Keyfactor API",
OrderedBy = "Brian Hill",
OrderDate = "21-Sep-2026",
ServerSoftware = "OTHER",
Certificate = apiCert
};
}

/// <summary>
/// Same uuid/order/key pair as <see cref="SingleNameBaseline" />, reappearing with a
/// mid-term reissue: isLifeCycleReIssueCertificate is true, effective/expiration shifted
/// forward to the reissue date, order validity window unchanged, and a *new certificate
/// re-signed from the same key* - a new serial number, not a new key pair.
/// </summary>
public static CertificateResponse SingleNameMidCycleReissue()
{
var baseline = SingleNameBaseline();
var key = GetOrCreateKey(baseline.Uuid);
baseline.Certificate = MakeApiCertificate(baseline.CommonName, key,
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(366));
baseline.EffectiveDate = "05-Nov-2026";
baseline.ExpirationDate = "23-May-2027";
baseline.OrderDate = "05-Nov-2026";
baseline.IsLifeCycleReIssueCertificate = true;
return baseline;
}

/// <summary>
/// Wildcard, multi-name DV certificate (*.boingy.com). Real uuid/metadata from a CSC
/// sample response; covers the multi-name product type, not just single-name DV.
/// </summary>
public static CertificateResponse WildcardMultiNameBaseline()
{
const string uuid = "7db327f6-34a0-4919-9af1-1a76277cc2a7";
var key = GetOrCreateKey(uuid);
var apiCert = MakeApiCertificate("*.boingy.com", key,
DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365));
return new CertificateResponse
{
Uuid = uuid,
CommonName = "*.boingy.com",
AdditionalNames = new List<string>(),
CertificateType = "CSC TrustedSecure DV Wildcard, Multiple Names",
Status = "ACTIVE",
EffectiveDate = "16-Sep-2026",
ExpirationDate = "03-Apr-2027",
OrderValidityStartDate = "13-Sep-2026",
OrderValidityEndDate = "13-Sep-2027",
IsRenewed = false,
IsLifeCycleReIssueCertificate = false,
BusinessUnit = "Keyfactor API",
OrderedBy = "Brian Hill",
OrderDate = "17-Sep-2026",
ServerSoftware = "OTHER",
Certificate = apiCert
};
}

/// <summary>Mid-cycle reissue of <see cref="WildcardMultiNameBaseline" />, re-signed from the same key.</summary>
public static CertificateResponse WildcardMultiNameMidCycleReissue()
{
var baseline = WildcardMultiNameBaseline();
var key = GetOrCreateKey(baseline.Uuid);
baseline.Certificate = MakeApiCertificate(baseline.CommonName, key,
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(366));
baseline.EffectiveDate = "02-Dec-2026";
baseline.ExpirationDate = "20-Jun-2027";
baseline.OrderDate = "02-Dec-2026";
baseline.IsLifeCycleReIssueCertificate = true;
return baseline;
}
}
20 changes: 20 additions & 0 deletions cscglobal-caplugin.MockServer/CSCGlobalCAPlugin.MockServer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Keyfactor.Extensions.CAPlugin.CSCGlobal.MockServer</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\cscglobal-caplugin\CSCGlobalCAPlugin.csproj" />
<ProjectReference Include="..\cscglobal-caplugin.MockData\CSCGlobalCAPlugin.MockData.csproj" />
</ItemGroup>

</Project>
146 changes: 146 additions & 0 deletions cscglobal-caplugin.MockServer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2021 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.

using System.Collections.Concurrent;
using System.Net;
using System.Text;
using Keyfactor.AnyGateway.Extensions;
using Keyfactor.Extensions.CAPlugin.CSCGlobal;
using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models;
using Keyfactor.Extensions.CAPlugin.CSCGlobal.MockData;
using Newtonsoft.Json;

// Starts a real HTTP server on localhost implementing CSC's list/get certificate endpoints,
// backed by MidCycleReissueFixtures, then runs the real (unmodified) CSCGlobalCAPlugin.Synchronize
// against it over an actual HTTP connection - not a mocked ICscGlobalClient. This is the "does it
// actually behave this way when sync runs for real" check that a unit test alone can't give you.

const string baseUrl = "http://localhost:58242/";

var results = new List<CertificateResponse>
{
MidCycleReissueFixtures.SingleNameBaseline(),
MidCycleReissueFixtures.SingleNameMidCycleReissue(),
MidCycleReissueFixtures.WildcardMultiNameBaseline(),
MidCycleReissueFixtures.WildcardMultiNameMidCycleReissue()
};

using var listener = new HttpListener();
listener.Prefixes.Add(baseUrl);
listener.Start();
Console.WriteLine($"Mock CSC server listening on {baseUrl}");

var serverCts = new CancellationTokenSource();
var serverTask = Task.Run(() => RunServerAsync(listener, results, serverCts.Token));

try
{
var config = new MockConfigProvider
{
CAConnectionData = new Dictionary<string, object>
{
[Constants.CscGlobalUrl] = baseUrl,
[Constants.CscGlobalApiKey] = "mock-api-key",
[Constants.BearerToken] = "mock-bearer-token"
}
};

var plugin = new CSCGlobalCAPlugin();
plugin.Initialize(config, new MockCertificateDataReader());

var buffer = new BlockingCollection<AnyCAPluginCertificate>();

Console.WriteLine("\nRunning a real Synchronize() call against the mock server...\n");
await plugin.Synchronize(buffer, null, true, CancellationToken.None);

var synced = buffer.ToList();
Console.WriteLine($"Synchronize queued {synced.Count} certificate(s):\n");
foreach (var cert in synced)
{
Console.WriteLine($" CARequestID: {cert.CARequestID}");
Console.WriteLine($" ProductID: {cert.ProductID}");
Console.WriteLine($" Status: {cert.Status}");
Console.WriteLine($" Certificate: {cert.Certificate?[..40]}...");
Console.WriteLine();
}

var byUuid = synced.GroupBy(c => c.CARequestID).Where(g => g.Count() > 1);
foreach (var group in byUuid)
{
var distinctCerts = group.Select(c => c.Certificate).Distinct().Count();
Console.WriteLine($"CARequestID {group.Key} appeared {group.Count()} time(s) " +
$"with {distinctCerts} distinct certificate value(s) - " +
"this is the mid-cycle-reissue collision: nothing above " +
"distinguishes the reissue from an ordinary re-sync.");
}
}
finally
{
serverCts.Cancel();
listener.Stop();
}

static async Task RunServerAsync(HttpListener listener, List<CertificateResponse> results, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
HttpListenerContext ctx;
try
{
ctx = await listener.GetContextAsync().WaitAsync(token);
}
catch (OperationCanceledException)
{
return;
}
catch (ObjectDisposedException)
{
return;
}

var path = ctx.Request.Url?.AbsolutePath ?? "";
Console.WriteLine($"[mock] {ctx.Request.HttpMethod} {ctx.Request.Url}");

object responseBody;
if (path == "/dbs/api/v2/tls/certificate")
{
responseBody = new CertificateListResponse
{
Meta = new Meta { NumResults = results.Count },
Results = results
};
}
else if (path.StartsWith("/dbs/api/v2/tls/certificate/"))
{
var uuid = path[(path.LastIndexOf('/') + 1)..];
responseBody = results.FirstOrDefault(r => r.Uuid == uuid)
?? (object)new { error = "not found" };
}
else
{
ctx.Response.StatusCode = 404;
ctx.Response.Close();
continue;
}

var json = JsonConvert.SerializeObject(responseBody);
var bytes = Encoding.UTF8.GetBytes(json);
ctx.Response.ContentType = "application/json";
ctx.Response.ContentLength64 = bytes.Length;
await ctx.Response.OutputStream.WriteAsync(bytes);
ctx.Response.Close();
}
}

internal sealed class MockConfigProvider : IAnyCAPluginConfigProvider
{
public Dictionary<string, object> CAConnectionData { get; set; } = new();
}

internal sealed class MockCertificateDataReader : ICertificateDataReader
{
public Task<int> GetStatusByRequestID(string caRequestID) => Task.FromResult(0);
public Task<bool> DoesCertExistForRequestID(string caRequestID) => Task.FromResult(false);
public Task<string> GetRequestIDBySerialNumber(string serialNumber) => Task.FromResult(string.Empty);
public DateTime? GetExpirationDateByRequestId(string caRequestID) => null;
}
1 change: 1 addition & 0 deletions cscglobal-caplugin.Tests/CSCGlobalCAPlugin.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<ItemGroup>
<ProjectReference Include="..\cscglobal-caplugin\CSCGlobalCAPlugin.csproj" />
<ProjectReference Include="..\cscglobal-caplugin.MockData\CSCGlobalCAPlugin.MockData.csproj" />
</ItemGroup>

</Project>
Loading
Loading