diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 64919a4..0f3d3ae 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,10 +11,17 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v3 + uses: keyfactor/actions/.github/workflows/starter.yml@v5 + with: + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} + command_hostname: ${{ vars.COMMAND_HOSTNAME }} + command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: token: ${{ secrets.V2BUILDTOKEN}} - APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} scan_token: ${{ secrets.SAST_TOKEN }} + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} diff --git a/HydrantCAProxy.Tests/HydrantCAProxy.Tests.csproj b/HydrantCAProxy.Tests/HydrantCAProxy.Tests.csproj new file mode 100644 index 0000000..6c8eb2b --- /dev/null +++ b/HydrantCAProxy.Tests/HydrantCAProxy.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + disable + disable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/HydrantCAProxy.Tests/RequestManagerTests.cs b/HydrantCAProxy.Tests/RequestManagerTests.cs new file mode 100644 index 0000000..509df49 --- /dev/null +++ b/HydrantCAProxy.Tests/RequestManagerTests.cs @@ -0,0 +1,345 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain a +// copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + +using System; +using System.Collections.Generic; +using Keyfactor.AnyGateway.Extensions; +using Keyfactor.Extensions.CAPlugin.HydrantId; +using Keyfactor.HydrantId; +using Keyfactor.HydrantId.Client.Models; +using Keyfactor.HydrantId.Client.Models.Enums; +using Keyfactor.HydrantId.Exceptions; +using Keyfactor.PKI.Enums.EJBCA; +using Xunit; + +namespace HydrantCAProxy.Tests +{ + public class RequestManagerTests + { + private readonly RequestManager _sut = new RequestManager(); + + // A valid PEM CSR (CN=unit.test.hydrantid.local) used to exercise the + // enrollment/DN-parsing paths without contacting a live CA. + private const string SampleCsr = + "-----BEGIN CERTIFICATE REQUEST-----\n" + + "MIICyDCCAbACAQAwgYIxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARPaGlvMRUwEwYD\n" + + "VQQHDAxJbmRlcGVuZGVuY2UxEjAQBgNVBAoMCUtleWZhY3RvcjEVMBMGA1UECwwM\n" + + "SW50ZWdyYXRpb25zMSIwIAYDVQQDDBl1bml0LnRlc3QuaHlkcmFudGlkLmxvY2Fs\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7HMrgfnq6o9t+7NAI4wZ\n" + + "XmiIY3lQcuEA2drbwDqx1HW78xbs6ajhIO8A68RpHUjdBfgOl+3zwCcjgbi8+whI\n" + + "OHubyMsonPCvCoKVUNv1CBclDcKEf+zAFuc7TWeL8n9aZNIeI/mLqhDxt2ZPIPuC\n" + + "tNh1wZToQ5gf4u/LQXSksLwbiITeBsATEKGNMsTERM7gYuldPQFS3bTof7LGRPWT\n" + + "shwNiBv6dw5QIgmXOBSJWdT0NfWVNudTF1wxV+41E/mvQCM+66Onw+ialH1nRefh\n" + + "LCiWIT48LLHLrYN045QorzqbDPzk8itpka+6JA04rlNKcSOBurAypkWBvhnU9N8F\n" + + "pQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBADO6dln9VOVkCG5qTBuifSxrGgDt\n" + + "IoQFIHxtMVhMI2CiPPeDDfJpPDX7CoHKRGKelilwxnWlOfzupv1Qb/02YXXq/F/Z\n" + + "twSyVAIbisuzL6RLIGox3GSkwlM0JTiyjASUJyVextRvxlmMRWTdc4z2v7Wxgmbf\n" + + "k8wZ7VrUYofBmAj9S3ozilPWRKspl/BZrm+4IIoufa2BKfMnGQGbsad22mrpkRtG\n" + + "1gm6iZDzaVTSC3iO5+CA/ZNwRT2ShIAHAbZTUSf62n5+nfs8Wki67i96hQqX7qIT\n" + + "MRXVBIV6K2c9Ls9aEh5qnPR8wre/VMaufCliSb0Q4X50Tal8kJZbS6/ZfJo=\n" + + "-----END CERTIFICATE REQUEST-----"; + + private static EnrollmentProductInfo ProductInfo(Dictionary parameters) => + new EnrollmentProductInfo { ProductID = "test-policy", ProductParameters = parameters }; + + // --------------------------------------------------------------------- + // ResolveTemplateParameter — the ADO 84076 / 81803 fix. + // Command does not populate a template's parameter collection with the + // annotation defaults until the template is saved. The resolver must fall + // back to the declared DefaultValue so enrollment still works. + // --------------------------------------------------------------------- + + [Theory] + [InlineData("ValidityPeriod", "Years")] + [InlineData("ValidityUnits", "1")] + [InlineData("RenewalDays", "30")] + public void ResolveTemplateParameter_KeyMissing_ReturnsAnnotationDefault(string key, string expected) + { + // Unsaved template: Command supplies an empty parameter collection. + var productInfo = ProductInfo(new Dictionary()); + + var result = RequestManager.ResolveTemplateParameter(productInfo, key); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void ResolveTemplateParameter_BlankValue_ReturnsAnnotationDefault(string blank) + { + var productInfo = ProductInfo(new Dictionary { ["ValidityPeriod"] = blank }); + + var result = RequestManager.ResolveTemplateParameter(productInfo, "ValidityPeriod"); + + Assert.Equal("Years", result); + } + + [Fact] + public void ResolveTemplateParameter_ValueSupplied_ReturnsSuppliedValue() + { + var productInfo = ProductInfo(new Dictionary { ["ValidityPeriod"] = "Days" }); + + var result = RequestManager.ResolveTemplateParameter(productInfo, "ValidityPeriod"); + + Assert.Equal("Days", result); + } + + [Fact] + public void ResolveTemplateParameter_NullProductParameters_ReturnsAnnotationDefault() + { + var productInfo = ProductInfo(null); + + var result = RequestManager.ResolveTemplateParameter(productInfo, "RenewalDays"); + + Assert.Equal("30", result); + } + + [Fact] + public void ResolveTemplateParameter_NullProductInfo_ReturnsAnnotationDefault() + { + var result = RequestManager.ResolveTemplateParameter(null, "ValidityUnits"); + + Assert.Equal("1", result); + } + + [Fact] + public void ResolveTemplateParameter_UnknownKeyWithNoDefault_ReturnsNull() + { + var productInfo = ProductInfo(new Dictionary()); + + var result = RequestManager.ResolveTemplateParameter(productInfo, "NotADeclaredParameter"); + + Assert.Null(result); + } + + // --------------------------------------------------------------------- + // GetEnrollmentRequest — proves the resolver is actually wired into + // enrollment: an unsaved template (no parameters) still produces a valid + // request using the annotation defaults instead of throwing. + // --------------------------------------------------------------------- + + [Fact] + public void GetEnrollmentRequest_UnsavedTemplate_UsesDefaultValidity() + { + var productInfo = ProductInfo(new Dictionary()); + + var request = _sut.GetEnrollmentRequest(Guid.NewGuid(), productInfo, SampleCsr, null); + + // Defaults are ValidityPeriod=Years, ValidityUnits=1. + Assert.NotNull(request); + Assert.Equal(1, request.Validity.Years); + Assert.Null(request.Validity.Months); + Assert.Null(request.Validity.Days); + } + + [Fact] + public void GetEnrollmentRequest_SuppliedValidity_HonorsSuppliedValues() + { + var productInfo = ProductInfo(new Dictionary + { + ["ValidityPeriod"] = "Months", + ["ValidityUnits"] = "6" + }); + + var request = _sut.GetEnrollmentRequest(Guid.NewGuid(), productInfo, SampleCsr, null); + + Assert.Equal(6, request.Validity.Months); + Assert.Null(request.Validity.Years); + } + + [Fact] + public void GetEnrollmentRequest_NullCsr_Throws() + { + var productInfo = ProductInfo(new Dictionary()); + + Assert.Throws(() => _sut.GetEnrollmentRequest(Guid.NewGuid(), productInfo, null, null)); + } + + [Fact] + public void GetEnrollmentRequest_NullProductInfo_Throws() + { + Assert.Throws(() => _sut.GetEnrollmentRequest(Guid.NewGuid(), null, SampleCsr, null)); + } + + [Fact] + public void GetEnrollmentRequest_WithSans_PopulatesSubjectAltNames() + { + var productInfo = ProductInfo(new Dictionary()); + var sans = new Dictionary { ["dnsname"] = new[] { "a.example.com", "b.example.com" } }; + + var request = _sut.GetEnrollmentRequest(Guid.NewGuid(), productInfo, SampleCsr, sans); + + Assert.NotNull(request.SubjectAltNames); + Assert.Equal(2, request.SubjectAltNames.Dnsname.Count); + } + + // --------------------------------------------------------------------- + // GetMapRevokeReasons — revocation reason mapping (incl. ADO 86120 + // reason 0 = Unspecified). + // --------------------------------------------------------------------- + + [Theory] + [InlineData(0u, RevocationReasons.Unspecified)] + [InlineData(1u, RevocationReasons.KeyCompromise)] + [InlineData(3u, RevocationReasons.AffiliationChanged)] + [InlineData(4u, RevocationReasons.Superseded)] + [InlineData(5u, RevocationReasons.CessationOfOperation)] + public void GetMapRevokeReasons_SupportedReason_MapsCorrectly(uint input, RevocationReasons expected) + { + Assert.Equal(expected, _sut.GetMapRevokeReasons(input)); + } + + [Theory] + [InlineData(2u)] // certificateHold — not supported + [InlineData(6u)] + [InlineData(99u)] + public void GetMapRevokeReasons_UnsupportedReason_Throws(uint input) + { + Assert.Throws(() => _sut.GetMapRevokeReasons(input)); + } + + [Fact] + public void GetRevokeRequest_SetsReason() + { + var result = _sut.GetRevokeRequest(RevocationReasons.KeyCompromise); + + Assert.Equal(RevocationReasons.KeyCompromise, result.Reason); + } + + // --------------------------------------------------------------------- + // GetMapReturnStatus — HydrantId status -> Keyfactor EndEntityStatus. + // --------------------------------------------------------------------- + + [Theory] + [InlineData(RevocationStatusEnum.Valid, EndEntityStatus.GENERATED)] + [InlineData(RevocationStatusEnum.Pending, EndEntityStatus.INPROCESS)] + [InlineData(RevocationStatusEnum.Revoked, EndEntityStatus.REVOKED)] + [InlineData(RevocationStatusEnum.Failed, EndEntityStatus.FAILED)] + [InlineData(RevocationStatusEnum.Expired, EndEntityStatus.FAILED)] // default branch + public void GetMapReturnStatus_MapsToExpectedEndEntityStatus(RevocationStatusEnum input, EndEntityStatus expected) + { + Assert.Equal((int)expected, _sut.GetMapReturnStatus(input)); + } + + // --------------------------------------------------------------------- + // GetRenewalRequest + // --------------------------------------------------------------------- + + [Fact] + public void GetRenewalRequest_WithCsr_SetsCsrAndReuseFlag() + { + var result = _sut.GetRenewalRequest("some-csr", reuseCsr: false); + + Assert.Equal("some-csr", result.Csr); + Assert.False(result.ReuseCsr); + } + + [Fact] + public void GetRenewalRequest_ReuseCsrWithoutCsr_DoesNotThrow() + { + var result = _sut.GetRenewalRequest(null, reuseCsr: true); + + Assert.True(result.ReuseCsr); + } + + [Fact] + public void GetRenewalRequest_NoCsrAndNoReuse_Throws() + { + Assert.Throws(() => _sut.GetRenewalRequest(null, reuseCsr: false)); + } + + // --------------------------------------------------------------------- + // GetSansRequest + // --------------------------------------------------------------------- + + [Fact] + public void GetSansRequest_Null_ReturnsEmptySans() + { + var result = _sut.GetSansRequest(null); + + Assert.NotNull(result); + } + + [Fact] + public void GetSansRequest_AllTypes_Populated() + { + var sans = new Dictionary + { + ["dnsname"] = new[] { "example.com" }, + ["ipaddress"] = new[] { "10.0.0.1", "10.0.0.2" }, + ["rfc822name"] = new[] { "user@example.com" }, + ["upn"] = new[] { "user@corp.local" } + }; + + var result = _sut.GetSansRequest(sans); + + Assert.Single(result.Dnsname); + Assert.Equal(2, result.Ipaddress.Count); + Assert.Single(result.Rfc822Name); + Assert.Single(result.Upn); + } + + // --------------------------------------------------------------------- + // GetCertificatesListRequest + // --------------------------------------------------------------------- + + [Fact] + public void GetCertificatesListRequest_SetsOffsetAndLimit() + { + var result = _sut.GetCertificatesListRequest(offset: 100, limit: 50); + + Assert.Equal(100, result.Offset); + Assert.Equal(50, result.Limit); + Assert.True(result.Expired); + } + + // --------------------------------------------------------------------- + // GetEnrollmentResult + // --------------------------------------------------------------------- + + [Fact] + public void GetEnrollmentResult_NullEnrollmentResult_ReturnsFailed() + { + var result = _sut.GetEnrollmentResult(null, new AnyCAPluginCertificate { Certificate = "cert" }); + + Assert.Equal((int)EndEntityStatus.FAILED, result.Status); + } + + [Fact] + public void GetEnrollmentResult_MissingId_ReturnsFailed() + { + var cert = new Certificate { Id = null }; + + var result = _sut.GetEnrollmentResult(cert, new AnyCAPluginCertificate { Certificate = "cert" }); + + Assert.Equal((int)EndEntityStatus.FAILED, result.Status); + } + + [Fact] + public void GetEnrollmentResult_MissingCertificateContent_ReturnsFailed() + { + var cert = new Certificate { Id = Guid.NewGuid() }; + + var result = _sut.GetEnrollmentResult(cert, new AnyCAPluginCertificate { Certificate = "" }); + + Assert.Equal((int)EndEntityStatus.FAILED, result.Status); + } + + [Fact] + public void GetEnrollmentResult_Valid_ReturnsGenerated() + { + var id = Guid.NewGuid(); + var cert = new Certificate { Id = id }; + var pluginCert = new AnyCAPluginCertificate { Certificate = "BASE64CERT" }; + + var result = _sut.GetEnrollmentResult(cert, pluginCert); + + Assert.Equal((int)EndEntityStatus.GENERATED, result.Status); + Assert.Equal(id.ToString(), result.CARequestID); + Assert.Equal("BASE64CERT", result.Certificate); + } + } +} diff --git a/HydrantCAProxy/HydrantIdCAPlugin.cs b/HydrantCAProxy/HydrantIdCAPlugin.cs index cc7b611..6c7f86e 100644 --- a/HydrantCAProxy/HydrantIdCAPlugin.cs +++ b/HydrantCAProxy/HydrantIdCAPlugin.cs @@ -590,17 +590,21 @@ await flow.StepAsync("SubmitEnrollment", async () => var expiration = previousX509.NotAfter; var now = DateTime.UtcNow; - if (!productInfo.ProductParameters.ContainsKey("RenewalDays")) + // Resolve RenewalDays from the supplied parameters, falling back to the + // annotation default when Command has not yet populated it (unsaved + // template — ADO 81803). Only fail if there is no default either. + var renewalDays = RequestManager.ResolveTemplateParameter(productInfo, HydrantIdCAPluginConfig.EnrollmentParametersConstants.RenewalDays); + if (string.IsNullOrWhiteSpace(renewalDays)) { - flow.Fail("ValidateRenewalDays", "RenewalDays not found in ProductParameters"); + flow.Fail("ValidateRenewalDays", "RenewalDays not supplied and no annotation default is defined"); return new EnrollmentResult { Status = (int)EndEntityStatus.FAILED, - StatusMessage = "Renewal failed: RenewalDays not found in product parameters." + StatusMessage = "Renewal failed: RenewalDays not found in product parameters and no default is defined." }; } - var isRenewal = (expiration - now).TotalDays <= Convert.ToInt16(productInfo.ProductParameters["RenewalDays"]); + var isRenewal = (expiration - now).TotalDays <= Convert.ToInt16(renewalDays); _logger.LogTrace("Enroll: expiration={Expiration}, now={Now}, isRenewal={IsRenewal}", expiration, now, isRenewal); flow.Step("DetermineRenewOrReissue", isRenewal ? "Renewal" : "Re-Issue"); diff --git a/HydrantCAProxy/HydrantIdCAPlugin.csproj b/HydrantCAProxy/HydrantIdCAPlugin.csproj index fef6cf3..d2ea4cd 100644 --- a/HydrantCAProxy/HydrantIdCAPlugin.csproj +++ b/HydrantCAProxy/HydrantIdCAPlugin.csproj @@ -8,7 +8,7 @@ HydrantIdCAPlugin - + diff --git a/HydrantCAProxy/RequestManager.cs b/HydrantCAProxy/RequestManager.cs index 6a94b75..f2fe8fa 100644 --- a/HydrantCAProxy/RequestManager.cs +++ b/HydrantCAProxy/RequestManager.cs @@ -21,6 +21,7 @@ using LogHandler = Keyfactor.Logging.LogHandler; using Keyfactor.AnyGateway.Extensions; using Keyfactor.PKI.Enums.EJBCA; +using Keyfactor.Extensions.CAPlugin.HydrantId; using System.Linq; namespace Keyfactor.HydrantId @@ -29,6 +30,41 @@ public class RequestManager { private static readonly ILogger Log = LogHandler.GetClassLogger(); + // Default values for the template parameters declared in + // GetTemplateParameterAnnotations(). Keyfactor Command does not populate a + // template's parameter collection with these annotation defaults until the + // template has been saved, so an enrollment against a template that was added + // but never saved arrives with the keys absent. Falling back to these defaults + // keeps enrollment working in that state instead of throwing + // ("The given key ... was not present in the dictionary"). See ADO 81803 / 84076. + private static readonly Dictionary TemplateParameterDefaults = + HydrantIdCAPluginConfig.GetTemplateParameterAnnotations() + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DefaultValue); + + /// + /// Returns the enrollment parameter value supplied by Command, falling back to the + /// default declared in + /// when the key is absent or blank. Returns null only when neither a supplied value + /// nor an annotation default exists. + /// + public static string ResolveTemplateParameter(EnrollmentProductInfo productInfo, string key) + { + if (productInfo?.ProductParameters != null && + productInfo.ProductParameters.TryGetValue(key, out var supplied) && + !string.IsNullOrWhiteSpace(supplied)) + { + return supplied; + } + + if (TemplateParameterDefaults.TryGetValue(key, out var def) && def != null) + { + Log.LogTrace("ResolveTemplateParameter: '{Key}' not supplied by Command; using annotation default '{Default}'", key, def); + return Convert.ToString(def); + } + + return null; + } + public int GetMapReturnStatus(RevocationStatusEnum hydrantIdStatus) { try @@ -160,22 +196,25 @@ public CertRequestBody GetEnrollmentRequest(Guid? policyId, EnrollmentProductInf if (productInfo == null) throw new ArgumentNullException(nameof(productInfo), "productInfo cannot be null."); - if (productInfo.ProductParameters == null) - throw new ArgumentNullException(nameof(productInfo), "productInfo.ProductParameters cannot be null."); if (string.IsNullOrEmpty(csr)) throw new ArgumentNullException(nameof(csr), "CSR cannot be null or empty."); - if (!productInfo.ProductParameters.ContainsKey("ValidityPeriod")) - throw new ArgumentException("ValidityPeriod is required in ProductParameters.", nameof(productInfo)); - if (!productInfo.ProductParameters.ContainsKey("ValidityUnits")) - throw new ArgumentException("ValidityUnits is required in ProductParameters.", nameof(productInfo)); + // Resolve validity from the supplied parameters, falling back to annotation + // defaults when Command has not yet populated them (unsaved template — ADO 81803). + var validityPeriod = ResolveTemplateParameter(productInfo, HydrantIdCAPluginConfig.EnrollmentParametersConstants.ValidityPeriod); + var validityUnits = ResolveTemplateParameter(productInfo, HydrantIdCAPluginConfig.EnrollmentParametersConstants.ValidityUnits); + + if (string.IsNullOrWhiteSpace(validityPeriod)) + throw new ArgumentException("ValidityPeriod was not supplied and no annotation default is defined.", nameof(productInfo)); + if (string.IsNullOrWhiteSpace(validityUnits)) + throw new ArgumentException("ValidityUnits was not supplied and no annotation default is defined.", nameof(productInfo)); var request = new CertRequestBody { Policy = policyId, Csr = csr, DnComponents = GetDnComponentsRequest(csr), - Validity = GetValidity(productInfo.ProductParameters["ValidityPeriod"], Convert.ToInt16(productInfo.ProductParameters["ValidityUnits"])) + Validity = GetValidity(validityPeriod, Convert.ToInt16(validityUnits)) }; if (san != null && san.Count > 0) diff --git a/HydrantIdCAProxy.sln b/HydrantIdCAProxy.sln index ed1d538..96b4291 100644 --- a/HydrantIdCAProxy.sln +++ b/HydrantIdCAProxy.sln @@ -14,19 +14,59 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\..\README.md = ..\..\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HydrantCAProxy.Tests", "HydrantCAProxy.Tests\HydrantCAProxy.Tests.csproj", "{5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HydrantCAProxy", "HydrantCAProxy", "{3E513467-2F3E-ABB0-F7F2-0CA04438089D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Prerelease|Any CPU = Prerelease|Any CPU + Prerelease|x64 = Prerelease|x64 + Prerelease|x86 = Prerelease|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Debug|Any CPU.Build.0 = Debug|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Debug|x64.ActiveCfg = Debug|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Debug|x64.Build.0 = Debug|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Debug|x86.ActiveCfg = Debug|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Debug|x86.Build.0 = Debug|Any CPU {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Prerelease|Any CPU.ActiveCfg = Release|Any CPU {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Prerelease|Any CPU.Build.0 = Release|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Prerelease|x64.ActiveCfg = Prerelease|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Prerelease|x64.Build.0 = Prerelease|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Prerelease|x86.ActiveCfg = Prerelease|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Prerelease|x86.Build.0 = Prerelease|Any CPU {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Release|Any CPU.ActiveCfg = Release|Any CPU {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Release|Any CPU.Build.0 = Release|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Release|x64.ActiveCfg = Release|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Release|x64.Build.0 = Release|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Release|x86.ActiveCfg = Release|Any CPU + {011DC646-BEF9-4D3B-9D20-CA444A26B355}.Release|x86.Build.0 = Release|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Debug|x64.Build.0 = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Debug|x86.Build.0 = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Prerelease|Any CPU.Build.0 = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Prerelease|x64.ActiveCfg = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Prerelease|x64.Build.0 = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Prerelease|x86.ActiveCfg = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Prerelease|x86.Build.0 = Debug|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Release|Any CPU.Build.0 = Release|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Release|x64.ActiveCfg = Release|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Release|x64.Build.0 = Release|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Release|x86.ActiveCfg = Release|Any CPU + {5DAFF413-A3CE-4451-BCEE-0AD9C6C09D41}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 2fa1217..124566b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Support - + · Requirements @@ -33,7 +33,6 @@

- The HID Global HydrantId AnyCA Gateway REST plugin extends the capabilities of HydrantId Certificate Authority Service to Keyfactor Command via the Keyfactor AnyCA Gateway. This plugin leverages the HydrantId REST API with Hawk authentication to provide comprehensive certificate lifecycle management. The plugin represents a fully featured AnyCA Plugin with the following capabilities: * **CA Sync**: @@ -52,10 +51,10 @@ The HID Global HydrantId AnyCA Gateway REST plugin extends the capabilities of H ## Compatibility -The HID Global AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later. +The HID Global AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2 and later. ## Support -The HID Global AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The HID Global AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. > To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. @@ -175,16 +174,17 @@ The plugin supports the following standard CRL revocation reasons: 2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [HID Global AnyCA Gateway REST plugin](https://github.com/Keyfactor/hydrantid-caplugin/releases/latest) from GitHub. -3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory: +3. Copy the unzipped directory (usually called `net6.0` or `net8.0` or `net10.0`) to the Extensions directory: ```shell Depending on your AnyCA Gateway REST version, copy the unzipped directory to one of the following locations: Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions + Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net10.0\Extensions ``` - > The directory containing the HID Global AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory. + > The directory containing the HID Global AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0` or `net10.0`) can be named anything, as long as it is unique within the `Extensions` directory. 4. Restart the AnyCA Gateway REST service. @@ -197,17 +197,17 @@ The plugin supports the following standard CRL revocation reasons: * **Gateway Registration** ### CA Connection Configuration - + When registering the HydrantId CA in the AnyCA Gateway, you'll need to provide the following configuration parameters: - + | Parameter | Description | Required | Example | |-----------|-------------|----------|---------| | **HydrantIdBaseUrl** | Full URL to the HydrantId API endpoint | Yes | `https://acm.hydrantid.com` or `https://acm-stage.hydrantid.com` | | **HydrantIdAuthId** | API Authentication ID provided by HydrantId | Yes | `your-auth-id` | | **HydrantIdAuthKey** | API Authentication Key provided by HydrantId | Yes | `your-secret-auth-key` | - + ### Gateway Registration Notes - + - Each defined Certificate Authority in the AnyCA Gateway REST can support one HydrantId API endpoint - If you have multiple HydrantId environments or accounts, you must define multiple Certificate Authorities in the AnyCA Gateway - Each CA configuration will manifest in Command as a separate CA entry @@ -218,75 +218,74 @@ The plugin supports the following standard CRL revocation reasons: - Certificate status mapping - End-entity certificate extraction from PEM chains - Enrollment completion polling (30-second timeout) - + ### Security Considerations - + 1. **Credential Storage**: Store API credentials securely and restrict access to the Gateway configuration 2. **Secret Management**: Consider using a secrets management system for AuthKey storage 3. **Network Security**: Ensure TLS/SSL is properly configured for all API communications 4. **Least Privilege**: Request API credentials with minimal required permissions 5. **Audit Logging**: Enable comprehensive logging in both the Gateway and HydrantId for security monitoring 6. **Credential Rotation**: Regularly rotate API credentials according to your security policy - + **CA Connection** - + Populate using the configuration fields collected in the [requirements](#requirements) section. - + * **HydrantIdBaseUrl** - The base URL for the HydrantId API endpoint. For example, `https://acm.hydrantid.com` or `https://acm-stage.hydrantid.com`. * **HydrantIdAuthId** - The API Authentication ID provided by HydrantId for API access. * **HydrantIdAuthKey** - The API Authentication Key (secret) provided by HydrantId for API access. - + 2. **Certificate Template Configuration** - + After adding the CA to the Gateway, configure each certificate template: - + 1. Navigate to the Templates/Products section for the newly added CA 2. For each template (policy) discovered from HydrantId, configure: - **ValidityPeriod**: Select `Days`, `Months`, or `Years` - **ValidityUnits**: Enter the numeric value (e.g., `365` for one year in days) - **RenewalDays**: Enter the renewal window in days (e.g., `30`) - + Example configurations: - **1-Year Certificate (Days)**: ValidityPeriod=`Days`, ValidityUnits=`365`, RenewalDays=`30` - **2-Year Certificate (Years)**: ValidityPeriod=`Years`, ValidityUnits=`2`, RenewalDays=`60` - **6-Month Certificate (Months)**: ValidityPeriod=`Months`, ValidityUnits=`6`, RenewalDays=`30` - + 3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates. * **CA Connection** Populate using the configuration fields collected in the [requirements](#requirements) section. - * **HydrantIdBaseUrl** - The Base URL For the HydrantId Endpoint similar to https://acm-stage.hydrantid.com. Get this from HydrantId. - * **HydrantIdAuthId** - The AuthId Obtained from HydrantId. - * **HydrantIdAuthKey** - The AuthKey Obtained from HydrantId. - * **Enabled** - Flag to Enable or Disable the CA connector. + * **HydrantIdBaseUrl** - The Base URL For the HydrantId Endpoint similar to https://acm-stage.hydrantid.com. Get this from HydrantId. + * **HydrantIdAuthId** - The AuthId Obtained from HydrantId. + * **HydrantIdAuthKey** - The AuthKey Obtained from HydrantId. + * **Enabled** - Flag to Enable or Disable the CA connector. 2. ### Template (Product) Configuration - Each certificate template (policy) discovered from HydrantId requires configuration for enrollment: + Each certificate template (policy) discovered from HydrantId requires configuration for enrollment: - | Parameter | Description | Required | Example | - |-----------|-------------|----------|---------| - | **ValidityPeriod** | Time unit for certificate lifetime | Yes | `Days`, `Months`, or `Years` | - | **ValidityUnits** | Numeric value for the validity period | Yes | `365` (for 1 year in days), `12` (for 1 year in months), `2` (for 2 years) | - | **RenewalDays** | Days before expiration to trigger renewal | Yes | `30` (renew within 30 days of expiration) | + | Parameter | Description | Required | Example | + |-----------|-------------|----------|---------| + | **ValidityPeriod** | Time unit for certificate lifetime | Yes | `Days`, `Months`, or `Years` | + | **ValidityUnits** | Numeric value for the validity period | Yes | `365` (for 1 year in days), `12` (for 1 year in months), `2` (for 2 years) | + | **RenewalDays** | Days before expiration to trigger renewal | Yes | `30` (renew within 30 days of expiration) | - **Important Notes:** - - Template names (Product IDs) are automatically discovered from HydrantId using the GET /api/v2/policies endpoint - - The ValidityPeriod and ValidityUnits combine to determine the certificate lifetime - - RenewalDays determines the behavior for certificate renewal: - - Within window: Performs a renewal operation (maintains certificate lineage) - - Outside window: Performs a re-issue operation (new certificate enrollment) + **Important Notes:** + - Template names (Product IDs) are automatically discovered from HydrantId using the GET /api/v2/policies endpoint + - The ValidityPeriod and ValidityUnits combine to determine the certificate lifetime + - RenewalDays determines the behavior for certificate renewal: + - Within window: Performs a renewal operation (maintains certificate lineage) + - Outside window: Performs a re-issue operation (new certificate enrollment) 3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates. 4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters: - * **ValidityPeriod** - The desired lifetime time period could be Days, Months or Years. - * **ValidityUnits** - The desired lifetime time value some number indicating days, months or years. - * **RenewalDays** - The window that determines whether it is a renewal vs a re-issue. - + * **ValidityPeriod** - The desired lifetime time period could be Days, Months or Years. + * **ValidityUnits** - The desired lifetime time value some number indicating days, months or years. + * **RenewalDays** - The window that determines whether it is a renewal vs a re-issue. ## Installation @@ -308,11 +307,10 @@ The plugin supports the following standard CRL revocation reasons: 5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the HID Global HydrantId plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal. - ## License Apache License 2.0, see [LICENSE](LICENSE). ## Related Integrations -See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway). \ No newline at end of file +See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway). diff --git a/integration-manifest.json b/integration-manifest.json index c8b6e47..3f9f33b 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -9,7 +9,7 @@ "support_level": "kf-supported", "link_github": true, "update_catalog": true, - "gateway_framework": "24.2", + "gateway_framework": "26.2", "about": { "carest": { "ca_plugin_config": [