From 5836b3457cddf1b2935a2ddda2eae280cb8beed8 Mon Sep 17 00:00:00 2001 From: David Galey Date: Fri, 25 Jul 2025 15:01:31 -0400 Subject: [PATCH 1/4] Add custom field support --- cscglobal-caplugin/CSCGlobalCAPlugin.cs | 10 +++--- cscglobal-caplugin/Client/CscGlobalClient.cs | 11 +++++++ .../Client/Models/GetCustomField.cs | 21 ++++++++++++ .../Interfaces/ICscGlobalClient.cs | 2 ++ .../Interfaces/IGetCustomField.cs | 13 ++++++++ cscglobal-caplugin/RequestManager.cs | 32 +++++++++++++++---- 6 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 cscglobal-caplugin/Client/Models/GetCustomField.cs create mode 100644 cscglobal-caplugin/Interfaces/IGetCustomField.cs diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs index 70ced27..bcc353a 100644 --- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs +++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs @@ -203,7 +203,9 @@ public async Task Enroll(string csr, string subject, Dictionar } string uUId; - switch (enrollmentType) + var customFields = await CscGlobalClient.SubmitGetCustomFields(); + + switch (enrollmentType) { case EnrollmentType.New: Logger.LogTrace("Entering New Enrollment"); @@ -211,7 +213,7 @@ public async Task Enroll(string csr, string subject, Dictionar IRegistrationResponse enrollmentResponse; if (!productInfo.ProductParameters.ContainsKey("PriorCertSN")) { - enrollmentRequest = _requestManager.GetRegistrationRequest(productInfo, csr, san); + enrollmentRequest = _requestManager.GetRegistrationRequest(productInfo, csr, san, customFields); Logger.LogTrace($"Enrollment Request JSON: {JsonConvert.SerializeObject(enrollmentRequest)}"); enrollmentResponse = Task.Run(async () => await CscGlobalClient.SubmitRegistrationAsync(enrollmentRequest)) @@ -253,7 +255,7 @@ public async Task Enroll(string csr, string subject, Dictionar uUId = await _certificateDataReader.GetRequestIDBySerialNumber( productInfo.ProductParameters["PriorCertSN"]); Logger.LogTrace($"Renew uUId: {uUId}"); - renewRequest = _requestManager.GetRenewalRequest(productInfo, uUId, csr, san); + renewRequest = _requestManager.GetRenewalRequest(productInfo, uUId, csr, san, customFields); Logger.LogTrace($"Renewal Request JSON: {JsonConvert.SerializeObject(renewRequest)}"); var renewResponse = Task.Run(async () => await CscGlobalClient.SubmitRenewalAsync(renewRequest)) .Result; @@ -278,7 +280,7 @@ public async Task Enroll(string csr, string subject, Dictionar productInfo.ProductParameters["PriorCertSN"]); uUId = requestid.Substring(0, 36); //uUId is a GUID Logger.LogTrace($"Reissue uUId: {uUId}"); - reissueRequest = _requestManager.GetReissueRequest(productInfo, uUId, csr, san); + reissueRequest = _requestManager.GetReissueRequest(productInfo, uUId, csr, san, customFields); Logger.LogTrace($"Reissue JSON: {JsonConvert.SerializeObject(reissueRequest)}"); var reissueResponse = Task.Run(async () => await CscGlobalClient.SubmitReissueAsync(reissueRequest)) .Result; diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs index ad6aac3..79474da 100644 --- a/cscglobal-caplugin/Client/CscGlobalClient.cs +++ b/cscglobal-caplugin/Client/CscGlobalClient.cs @@ -134,6 +134,17 @@ public async Task SubmitGetCertificateAsync(string certific } } + public async Task> SubmitGetCustomFields() + { + using (var resp = await RestClient.GetAsync($"/dbs/api/v2/admin/customfields")) + { + resp.EnsureSuccessStatusCode(); + var getCustomFieldsResponse = + JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + return getCustomFieldsResponse.CustomFields; + } + } + public async Task SubmitRevokeCertificateAsync(string uuId) { using (var resp = await RestClient.PutAsync($"/dbs/api/v2/tls/revoke/{uuId}", new StringContent(""))) diff --git a/cscglobal-caplugin/Client/Models/GetCustomField.cs b/cscglobal-caplugin/Client/Models/GetCustomField.cs new file mode 100644 index 0000000..7509746 --- /dev/null +++ b/cscglobal-caplugin/Client/Models/GetCustomField.cs @@ -0,0 +1,21 @@ +// 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. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using Keyfactor.Extensions.CAPlugin.CSCGlobal.Interfaces; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models; + +public class GetCustomField : IGetCustomField +{ + [JsonProperty("label")] public string Label { get; set; } + [JsonProperty("mandatory")] public bool Mandatory { get; set; } +} + +public class GetCustomFields +{ + [JsonProperty("customFields")] public List CustomFields { get; set; } +} \ No newline at end of file diff --git a/cscglobal-caplugin/Interfaces/ICscGlobalClient.cs b/cscglobal-caplugin/Interfaces/ICscGlobalClient.cs index 146c808..bdf7bc6 100644 --- a/cscglobal-caplugin/Interfaces/ICscGlobalClient.cs +++ b/cscglobal-caplugin/Interfaces/ICscGlobalClient.cs @@ -21,6 +21,8 @@ Task SubmitReissueAsync( Task SubmitGetCertificateAsync(string certificateId); + Task> SubmitGetCustomFields(); + Task SubmitCertificateListRequestAsync(); Task SubmitRevokeCertificateAsync(string uuId); diff --git a/cscglobal-caplugin/Interfaces/IGetCustomField.cs b/cscglobal-caplugin/Interfaces/IGetCustomField.cs new file mode 100644 index 0000000..e4ec799 --- /dev/null +++ b/cscglobal-caplugin/Interfaces/IGetCustomField.cs @@ -0,0 +1,13 @@ +// 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. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +namespace Keyfactor.Extensions.CAPlugin.CSCGlobal.Interfaces; + +public interface IGetCustomField +{ + string Label { get; set; } + bool Mandatory { get; set; } +} diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 41a1a83..2929649 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -12,6 +12,8 @@ using Keyfactor.PKI; using Keyfactor.PKI.Enums.EJBCA; +using Org.BouncyCastle.Bcpg; + namespace Keyfactor.Extensions.CAPlugin.CSCGlobal; public class RequestManager @@ -19,9 +21,25 @@ public class RequestManager public static Func Pemify = ss => ss.Length <= 64 ? ss : ss.Substring(0, 64) + "\n" + Pemify(ss.Substring(64)); - private List GetCustomFields(EnrollmentProductInfo productInfo) + private List GetCustomFields(EnrollmentProductInfo productInfo, List customFields) { var customFieldList = new List(); + foreach (var field in customFields) + { + if (productInfo.ProductParameters.ContainsKey(field.Label)) + { + var newField = new CustomField + { + Name = field.Label, + Value = productInfo.ProductParameters[field.Label] + }; + customFieldList.Add(newField); + } + else if (field.Mandatory) + { + throw new Exception($"Custom field {field.Label} is marked as mandatory, but was not supplied in the request."); + } + } return customFieldList; } @@ -116,7 +134,7 @@ public DomainControlValidation GetDomainControlValidation(string methodType, str } public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productInfo, string csr, - Dictionary sans) + Dictionary sans, List customFields) { //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; var cert = Pemify(csr); @@ -144,7 +162,7 @@ public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productI OrganizationContact = productInfo.ProductParameters["Organization Contact"], BusinessUnit = productInfo.ProductParameters["Business Unit"], ShowPrice = true, //User should not have to fill this out - CustomFields = GetCustomFields(productInfo), + CustomFields = GetCustomFields(productInfo, customFields), SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null, EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null }; @@ -190,7 +208,7 @@ public Notifications GetNotifications(EnrollmentProductInfo productInfo) } public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, string uUId, string csr, - Dictionary sans) + Dictionary sans, List customFields) { //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; var cert = Pemify(csr); @@ -219,7 +237,7 @@ public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, strin BusinessUnit = productInfo.ProductParameters["Business Unit"], ShowPrice = true, SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null, - CustomFields = GetCustomFields(productInfo), + CustomFields = GetCustomFields(productInfo, customFields), EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null }; } @@ -248,7 +266,7 @@ private List GetSubjectAlternativeNames(EnrollmentProduc } public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, string uUId, string csr, - Dictionary sans) + Dictionary sans, List customFields) { //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; var cert = Pemify(csr); @@ -277,7 +295,7 @@ public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, strin BusinessUnit = productInfo.ProductParameters["Business Unit"], ShowPrice = true, SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null, - CustomFields = GetCustomFields(productInfo), + CustomFields = GetCustomFields(productInfo, customFields), EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null }; } From 26957564ec1390cac9010ca71e7b532eddceb996 Mon Sep 17 00:00:00 2001 From: David Galey Date: Mon, 28 Jul 2025 11:45:57 -0400 Subject: [PATCH 2/4] changelog --- cscglobal-caplugin/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 cscglobal-caplugin/CHANGELOG.md diff --git a/cscglobal-caplugin/CHANGELOG.md b/cscglobal-caplugin/CHANGELOG.md new file mode 100644 index 0000000..ce88fa1 --- /dev/null +++ b/cscglobal-caplugin/CHANGELOG.md @@ -0,0 +1,8 @@ +v1.1.0 +- Support for custom fields in enrollment + +v1.0.1 +- Fixed issue with SANs not being read correctly. + +v1.0 +- Initial Release. \ No newline at end of file From 75e81661236d0894c1ae3cab0f19433d878b34ed Mon Sep 17 00:00:00 2001 From: David Galey Date: Fri, 5 Sep 2025 12:31:01 -0400 Subject: [PATCH 3/4] support cname return from enrollment --- cscglobal-caplugin/CHANGELOG.md | 1 + cscglobal-caplugin/RequestManager.cs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cscglobal-caplugin/CHANGELOG.md b/cscglobal-caplugin/CHANGELOG.md index ce88fa1..737b421 100644 --- a/cscglobal-caplugin/CHANGELOG.md +++ b/cscglobal-caplugin/CHANGELOG.md @@ -1,5 +1,6 @@ v1.1.0 - Support for custom fields in enrollment +- Support for returning CNAME tokens from enrollment call v1.0.1 - Fixed issue with SANs not being read correctly. diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 2929649..3317f7f 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -73,12 +73,22 @@ public EnrollmentResult StatusMessage = registrationResponse.RegistrationError.Description }; - return new EnrollmentResult + Dictionary cnames = new Dictionary(); + if (registrationResponse.Result.DcvDetails != null && registrationResponse.Result.DcvDetails.Count > 0) + { + foreach (var dcv in registrationResponse.Result.DcvDetails) + { + cnames.Add(dcv.CName.Name, dcv.CName.Value); + } + } + + return new EnrollmentResult { Status = (int)EndEntityStatus.EXTERNALVALIDATION, //success CARequestID = registrationResponse.Result.Status.Uuid, StatusMessage = - $"Order Successfully Created With Order Number {registrationResponse.Result.CommonName}" + $"Order Successfully Created With Order Number {registrationResponse.Result.CommonName}", + EnrollmentContext = (cnames.Count > 0) ? cnames : null }; } From 88123e11ed198829d34a1e6a4c1dbd2e1e963e5e Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Fri, 5 Sep 2025 16:34:29 +0000 Subject: [PATCH 4/4] Update generated docs --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ad40df..aa73d9a 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,16 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and 2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [CSCGlobal CA Gateway AnyCA Gateway REST plugin](https://github.com/Keyfactor/cscglobal-caplugin/releases/latest) from GitHub. -3. Copy the unzipped directory (usually called `net6.0`) to the Extensions directory: +3. Copy the unzipped directory (usually called `net6.0` or `net8.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 ``` - > The directory containing the CSCGlobal CA Gateway AnyCA Gateway REST plugin DLLs (`net6.0`) can be named anything, as long as it is unique within the `Extensions` directory. + > The directory containing the CSCGlobal CA Gateway AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory. 4. Restart the AnyCA Gateway REST service.