diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b8f82c..41b85e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,3 +14,13 @@ Fix for JSON serialization of revocation
1.1.0
Add support for using the cert upload feature to upload auth certs
Switch to .NET 8
+
+1.1.1
+Allow for manual specification of enrollment term length
+Add Lifetime parameter to allow for manual specification of cert validity
+Bugfix - Properly handle syncs of 0 records
+Allow for manual specification of enrollment term length
+
+1.1.2
+Add Lifetime parameter to allow for manual specification of cert validity
+Bugfix - Properly handle syncs of 0 records
diff --git a/README.md b/README.md
index 4b14544..782a9e5 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
- Sectigo Certificate Manager Gateway AnyCA Gateway REST Plugin
+ Sectigo Certificate Manager AnyCA Gateway REST Plugin
@@ -45,10 +45,10 @@ The Sectigo AnyCA Gateway REST plugin extends the capabilities of the Sectigo Ce
## Compatibility
-The Sectigo Certificate Manager Gateway AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
+The Sectigo Certificate Manager AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
## Support
-The Sectigo Certificate Manager Gateway 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 Sectigo Certificate Manager 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.
> 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.
@@ -61,7 +61,7 @@ In addition, for the admin account you plan to use, make sure it has the API adm
1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
-2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [Sectigo Certificate Manager Gateway AnyCA Gateway REST plugin](https://github.com/Keyfactor/sectigo-scm-caplugin/releases/latest) from GitHub.
+2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [Sectigo Certificate Manager AnyCA Gateway REST plugin](https://github.com/Keyfactor/sectigo-scm-caplugin/releases/latest) from GitHub.
3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory:
@@ -72,11 +72,11 @@ In addition, for the admin account you plan to use, make sure it has the API adm
Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
```
- > The directory containing the Sectigo Certificate Manager 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.
+ > The directory containing the Sectigo Certificate Manager 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.
-5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the Sectigo Certificate Manager Gateway plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
+5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the Sectigo Certificate Manager plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
## Configuration
@@ -113,6 +113,7 @@ In addition, for the admin account you plan to use, make sure it has the API adm
* **MultiDomain** - This flag lets Keyfactor know if the certificate can contain multiple domain names. Depending on the setting, the SAN entries of the request will change to support Sectigo requirements.
* **Organization** - If the organization name is provided here, the Sectigo gateway will use that organization name in requests instead of whatever is in the O= field in the request subject.
* **Department** - If your Sectigo account is using department-level products, put the appropriate department name here. Previously, this was alternatively supplied in the OU= subject field, which is now deprecated.
+ * **Lifetime** - OPTIONAL: The term length (in days) to use for enrollment. If not provided, the default is the first value available in the profile definition in your Sectigo account.
diff --git a/integration-manifest.json b/integration-manifest.json
index aba2711..8665460 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -78,6 +78,10 @@
{
"name": "Department",
"description": "If your Sectigo account is using department-level products, put the appropriate department name here. Previously, this was alternatively supplied in the OU= subject field, which is now deprecated."
+ },
+ {
+ "name": "Lifetime",
+ "description": "OPTIONAL: The term length (in days) to use for enrollment. If not provided, the default is the first value available in the profile definition in your Sectigo account."
}
]
}
diff --git a/sectigo-scm-caplugin/Client/SectigoClient.cs b/sectigo-scm-caplugin/Client/SectigoClient.cs
index f7da84f..583a233 100644
--- a/sectigo-scm-caplugin/Client/SectigoClient.cs
+++ b/sectigo-scm-caplugin/Client/SectigoClient.cs
@@ -8,6 +8,8 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using Org.BouncyCastle.Asn1.Ocsp;
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -19,6 +21,8 @@
using System.Threading;
using System.Threading.Tasks;
+using Error = Keyfactor.Extensions.CAPlugin.Sectigo.API.Error;
+
namespace Keyfactor.Extensions.CAPlugin.Sectigo.Client
{
public class SectigoClient
@@ -34,7 +38,9 @@ public SectigoClient(HttpClient client)
public async Task GetCertificate(int sslId)
{
- var response = await RestClient.GetAsync($"api/ssl/v1/{sslId}");
+ string url = $"api/ssl/v1/{sslId}";
+ Logger.LogTrace($"API Request: GET {url}");
+ var response = await RestClient.GetAsync(url);
return await ProcessResponse(response);
}
@@ -61,7 +67,10 @@ public async Task CertificateListProducer(BlockingCollection certs,
Logger.LogInformation($"Request Certificates at Position {certIndex} with Page Size {pageSize}");
certificatePageToProcess = await PageCertificates(certIndex, pageSize, filter);
Logger.LogDebug($"Found {certificatePageToProcess.Count} certificate to process");
-
+ if (certificatePageToProcess.Count == 0)
+ {
+ return;
+ }
//Processing Loop will add and retry adding to queue until all certificates have been processed for a page
batchCount = 0;
blockedCount = 0;
@@ -136,7 +145,7 @@ public async Task CertificateListProducer(BlockingCollection certs,
public async Task> PageCertificates(int position = 0, int size = 25, string filter = "")
{
string filterQueryString = string.IsNullOrEmpty(filter) ? string.Empty : $"&{filter}";
- Logger.LogTrace($"API Request: api/ssl/v1?position={position}&size={size}{filterQueryString}".TrimEnd());
+ Logger.LogTrace($"API Request: GET api/ssl/v1?position={position}&size={size}{filterQueryString}".TrimEnd());
var response = await RestClient.GetAsync($"api/ssl/v1?position={position}&size={size}{filterQueryString}".TrimEnd());
return await ProcessResponse>(response);
}
@@ -148,23 +157,17 @@ public async Task RevokeSslCertificateById(int sslId, int revcode, string
reasonCode = revcode,
reason = revreason
};
+ Logger.LogTrace($"API Request: POST api/ssl/v1/revoke/{sslId}\nParameters: {JsonConvert.SerializeObject(data, Formatting.Indented)}");
var response = await RestClient.PostAsJsonAsync($"api/ssl/v1/revoke/{sslId}", data);
- if (response.IsSuccessStatusCode)
- {
- return true;
- }
- var failedResp = ProcessResponse(response).Result;
- return failedResp.IsSuccess;//Should throw an exception with error message from API
+ var resp = ProcessResponse(response).Result;
+
+ return true;//Should throw an exception with error message from API, should only hit this if success
}
public async Task ListOrganizations()
{
+ Logger.LogTrace($"API Request: GET api/organization/v1");
var response = await RestClient.GetAsync("api/organization/v1");
- if (response.IsSuccessStatusCode)
- {
- string responseContent = await response.Content.ReadAsStringAsync();
- Logger.LogTrace($"Raw Response: {responseContent}");
- }
var orgsResponse = await ProcessResponse>(response);
return new ListOrganizationsResponse { Organizations = orgsResponse };
@@ -172,13 +175,8 @@ public async Task ListOrganizations()
public async Task GetOrganizationDetails(int orgId)
{
+ Logger.LogTrace($"API Request: GET api/organization/v1/{orgId}");
var response = await RestClient.GetAsync($"api/organization/v1/{orgId}");
- if (response.IsSuccessStatusCode)
- {
- string responseContent = await response.Content.ReadAsStringAsync();
- Logger.LogTrace($"Raw Response: {responseContent}");
- }
-
var orgDetailsResponse = await ProcessResponse(response);
return orgDetailsResponse;
}
@@ -200,6 +198,7 @@ public async Task ListPersons(int orgId)
public async Task ListCustomFields()
{
+ Logger.LogTrace($"API Request: GET api/ssl/v1/customFields");
var response = await RestClient.GetAsync("api/ssl/v1/customFields");
return new ListCustomFieldsResponse { CustomFields = await ProcessResponse>(response) };
}
@@ -211,13 +210,14 @@ public async Task ListSslProfiles(int? orgId = null)
{
urlSuffix = $"?organizationId={orgId}";
}
-
+ Logger.LogTrace($"API Request: GET api/ssl/v1/types{urlSuffix}");
var response = await RestClient.GetAsync($"api/ssl/v1/types{urlSuffix}");
return new ListSslProfilesResponse { SslProfiles = await ProcessResponse>(response) };
}
public async Task> PagePersons(int orgId, int position = 0, int size = 25)
{
+ Logger.LogTrace($"API Request: GET api/person/v1?position={position}&size={size}&organizationId={orgId}");
var response = await RestClient.GetAsync($"api/person/v1?position={position}&size={size}&organizationId={orgId}");
return await ProcessResponse>(response);
}
@@ -226,6 +226,7 @@ public async Task Enroll(EnrollRequest request)
{
try
{
+ Logger.LogTrace($"API Request: POST api/ssl/v1/enroll\nParameters: {JsonConvert.SerializeObject(request, Formatting.Indented)}");
var response = await RestClient.PostAsJsonAsync("api/ssl/v1/enroll", request);
var enrollResponse = await ProcessResponse(response);
@@ -245,35 +246,14 @@ public async Task Enroll(EnrollRequest request)
}
}
- public async Task Renew(int sslId)
- {
- try
- {
- var response = await RestClient.PostAsJsonAsync($"api/ssl/v1/renewById/{sslId}", "");
- var renewResponse = await ProcessResponse(response);
-
- return renewResponse.sslId;
- }
- catch (InvalidOperationException invalidOp)
- {
- throw new Exception($"Invalid Operation. {invalidOp.Message}|{invalidOp.StackTrace}");
- }
- catch (HttpRequestException httpEx)
- {
- throw new Exception($"HttpRequestException. {httpEx.Message}|{httpEx.StackTrace}");
- }
- catch (Exception)
- {
- throw;
- }
- }
-
public async Task PickupCertificate(int sslId, string subject)
{
+ Logger.LogTrace($"API Request: GET api/ssl/v1/collect/{sslId}/x509C0");
var response = await RestClient.GetAsync($"api/ssl/v1/collect/{sslId}/x509CO");
-
+
if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength > 0)
{
+ Logger.LogTrace($"Raw response: {response.Content.ReadAsStringAsync()}");
string pemChain = await response.Content.ReadAsStringAsync();
string[] splitChain = pemChain.Replace("\r\n", string.Empty).Split(new string[] { "-----" }, StringSplitOptions.RemoveEmptyEntries);
@@ -284,24 +264,19 @@ public async Task PickupCertificate(int sslId, string subject)
//return new X509Certificate2();
}
- public async Task Reissue(ReissueRequest request, int sslId)
- {
- var response = await RestClient.PostAsJsonAsync($"api/ssl/v1/replace/{sslId}", request);
- response.EnsureSuccessStatusCode();
- }
-
#region Static Methods
private static async Task ProcessResponse(HttpResponseMessage response)
{
+ string responseContent = await response.Content.ReadAsStringAsync();
+ Logger.LogDebug($"Raw API response: {responseContent}");
if (response.IsSuccessStatusCode)
{
- string responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject(responseContent);
}
else
{
- var error = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ var error = JsonConvert.DeserializeObject(responseContent);
throw new Exception($"{error.Code} | {error.Description}");
}
}
diff --git a/sectigo-scm-caplugin/Constants.cs b/sectigo-scm-caplugin/Constants.cs
index 014be4c..47f7af0 100644
--- a/sectigo-scm-caplugin/Constants.cs
+++ b/sectigo-scm-caplugin/Constants.cs
@@ -27,6 +27,7 @@ public class Config
public const string MULTIDOMAIN = "MultiDomain";
public const string ORGANIZATION = "Organization";
public const string DEPARTMENT = "Department";
+ public const string LIFETIME = "Lifetime";
}
//headers for API client
diff --git a/sectigo-scm-caplugin/SectigoCAPlugin.cs b/sectigo-scm-caplugin/SectigoCAPlugin.cs
index c88038b..dcb46db 100644
--- a/sectigo-scm-caplugin/SectigoCAPlugin.cs
+++ b/sectigo-scm-caplugin/SectigoCAPlugin.cs
@@ -196,6 +196,26 @@ public async Task Enroll(string csr, string subject, Dictionar
_logger.LogTrace($"Found {enrollmentProfile.name} profile for enroll request");
}
+ int termLength;
+ var profileTerms = Task.Run(async () => await GetProfileTerms(int.Parse(productInfo.ProductID))).Result;
+ if (!string.IsNullOrEmpty(productInfo.ProductParameters[Constants.Config.LIFETIME]))
+ {
+ var tempTerm = int.Parse(productInfo.ProductParameters[Constants.Config.LIFETIME]);
+ if (profileTerms.Contains(tempTerm))
+ {
+ termLength = tempTerm;
+ }
+ else
+ {
+ _logger.LogError($"Specified term length of {tempTerm} does not match available terms for product ID {productInfo.ProductID}. Available terms are {string.Join(",", profileTerms)}");
+ throw new Exception($"Specified term length of {tempTerm} does not match available terms for product ID {productInfo.ProductID}");
+ }
+ }
+ else
+ {
+ termLength = profileTerms[0];
+ }
+
int sslId;
string priorSn = string.Empty;
Certificate newCert = null;
@@ -216,7 +236,7 @@ public async Task Enroll(string csr, string subject, Dictionar
{
csr = csr,
orgId = requestOrgId,
- term = Task.Run(async () => await GetProfileTerm(int.Parse(productInfo.ProductID))).Result,
+ term = termLength,
certType = enrollmentProfile.id,
//External requestor is expected to be an email. Use config to pull the enrollment field or send blank
//sectigo will default to the account (API account) making the request.
@@ -229,8 +249,6 @@ public async Task Enroll(string csr, string subject, Dictionar
};
_logger.LogDebug($"Submit {enrollmentType} request");
- var jsonReq = JsonConvert.SerializeObject(request, Formatting.Indented);
- _logger.LogDebug($"Request object: {jsonReq}");
sslId = Task.Run(async () => await client.Enroll(request)).Result;
newCert = Task.Run(async () => await client.GetCertificate(sslId)).Result;
_logger.LogDebug($"Enrolled for Certificate {newCert.CommonName} (ID: {newCert.Id}) | Status: {newCert.status}. Attempt to Pickup Certificate.");
@@ -245,7 +263,7 @@ public async Task Enroll(string csr, string subject, Dictionar
catch (HttpRequestException httpEx)
{
_logger.LogError($"Enrollment Failed due to a HTTP error: {httpEx.Message}");
- throw new Exception(httpEx.Message);
+ throw;
}
catch (Exception ex)
{
@@ -257,7 +275,7 @@ public async Task Enroll(string csr, string subject, Dictionar
retError = ex.InnerException.Message;
}
- throw new Exception(retError);
+ throw;
}
}
@@ -431,6 +449,13 @@ public Dictionary GetTemplateParameterAnnotations()
Hidden = false,
DefaultValue = "",
Type = "String"
+ },
+ [Constants.Config.LIFETIME] = new PropertyConfigInfo()
+ {
+ Comments = "OPTIONAL: The term length (in days) to use for enrollment. If not provided, the default is the first value available in the profile definition in your Sectigo account.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
}
};
}
@@ -520,7 +545,7 @@ public async Task Synchronize(BlockingCollection blockin
_logger.LogError($"Synchronize task failed with the following message: {producerTask.Exception.Flatten().Message}");
throw producerTask.Exception.Flatten();
}
-
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Processing record {certToAdd.Id}");
string dbCertId = null;
int dbCertStatus = -1;
//serial number is blank on certs that have not been issued (awaiting approval)
@@ -566,23 +591,26 @@ public async Task Synchronize(BlockingCollection blockin
}
//Download to get full certdata required for sync process
- _logger.LogTrace($"Attempt to Pickup Certificate {certToAdd.CommonName} (ID: {certToAdd.Id})");
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Attempt to Pickup Certificate {certToAdd.CommonName}");
var certdataApi = Task.Run(async () => await client.PickupCertificate(certToAdd.Id, certToAdd.CommonName)).Result;
if (certdataApi != null)
certData = Convert.ToBase64String(certdataApi.GetRawCertData());
+
if (certToAdd == null || String.IsNullOrEmpty(certToAdd.SerialNumber) || String.IsNullOrEmpty(certToAdd.CommonName) || String.IsNullOrEmpty(certData))
{
_logger.LogDebug($"Certificate Data unavailable for {certToAdd.CommonName} (ID: {certToAdd.Id}). Skipping ");
continue;
}
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Retrieved cert data: {certData}");
string prodId = "";
try
{
- _logger.LogTrace($"Cert ID: {certToAdd.Id.ToString()}");
- _logger.LogTrace($"Sync ID: {syncReqId.ToString()}");
- _logger.LogTrace($"Product ID: {certToAdd.CertType.id.ToString()}");
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Cert ID: {certToAdd.Id.ToString()}");
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Sync ID: {syncReqId.ToString()}");
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Product ID: {certToAdd.CertType.id.ToString()}");
+ _logger.LogTrace($"SYNC TRACE ({certToAdd.Id}): Status: {certToAdd.status}");
prodId = certToAdd.CertType.id.ToString();
}
catch { }
@@ -674,11 +702,11 @@ private async Task GetOrganizationAsync(string orgName)
return orgList.Organizations.Where(x => x.name.ToLower().Equals(orgName.ToLower())).FirstOrDefault();
}
- private async Task GetProfileTerm(int profileId)
+ private async Task> GetProfileTerms(int profileId)
{
var client = SectigoClient.InitializeClient(_config, _certificateResolver);
var profileList = await client.ListSslProfiles();
- return profileList.SslProfiles.Where(x => x.id == profileId).FirstOrDefault().terms[0];
+ return profileList.SslProfiles.Where(x => x.id == profileId).FirstOrDefault().terms.ToList();
}
private async Task GetProfile(int profileId)