From a5581aa5f23a9c2b864f7380a46e2a3a94a08567 Mon Sep 17 00:00:00 2001 From: David Galey Date: Tue, 12 Jul 2022 09:02:12 -0400 Subject: [PATCH] fix for org detail lookup to verify cert configurations --- README.md.tpl | 2 +- .../API/ListOrganizationsResponse.cs | 56 +- .../API/OrganizationDetailsResponse.cs | 29 + src/SectigoCAProxy/Client/SectigoApiClient.cs | 576 +++++++++--------- src/SectigoCAProxy/SectigoCAProxy.cs | 57 +- src/SectigoCAProxy/SectigoCAProxy.csproj | 239 ++++---- 6 files changed, 521 insertions(+), 438 deletions(-) create mode 100644 src/SectigoCAProxy/API/OrganizationDetailsResponse.cs diff --git a/README.md.tpl b/README.md.tpl index 9ebc119..cad4942 100644 --- a/README.md.tpl +++ b/README.md.tpl @@ -10,7 +10,7 @@ * Certificates will only syncronize once. If a certificate is found based on Serial Number for the managed CA it will be skipped for subsequent syncs to minimize impact on Cert Manager API load * SSL Certificate Enrollment - * Note about organizations. The organization for enrollment is currently selected dynamically based on Organization and/or Org Unit of the CSR. If a top level Organization is found and is able to issue certs, that organization ID is passed with the enrollment request. If the Organization does not have any certificate types assigned, it will look for a department based on the OU name. If no matches are found the enrollment will fail as this is a required field for Sectigo. + * Note about organizations. The organization for enrollment is selected based on the Organization subject field, as well as any Department specified in the template configuration. If a department is specified, and that department exists within the organization and is valid for issuing certs, the department ID will be used. If no department is specified, the organization ID will be used if the organization is valid for issuing certs. If the organization/department are not valid for issuing certs, the enrollment will fail, as that is a required field for Sectigo. * SSL Certificate Revocation ### Not Implemented/Supported diff --git a/src/SectigoCAProxy/API/ListOrganizationsResponse.cs b/src/SectigoCAProxy/API/ListOrganizationsResponse.cs index 52133bf..918bcc6 100644 --- a/src/SectigoCAProxy/API/ListOrganizationsResponse.cs +++ b/src/SectigoCAProxy/API/ListOrganizationsResponse.cs @@ -1,11 +1,12 @@ -// 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. +// 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 Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Converters; + using System; using System.Collections.Generic; using System.Linq; @@ -13,25 +14,22 @@ using System.Threading.Tasks; namespace Keyfactor.AnyGateway.Sectigo.API -{ - public class ListOrganizationsResponse - { - public List Organizations { get; set; } - } - - public class Organization - { - public int id { get; set; } - public string name { get; set; } - public List certTypes { get; set; } - public List departments { get; set; } - } - - public class Department - { - public int id { get; set; } - public string name { get; set; } - public List certTypes { get; set; } - } - -} +{ + public class ListOrganizationsResponse + { + public List Organizations { get; set; } + } + + public class Organization + { + public int id { get; set; } + public string name { get; set; } + public List departments { get; set; } + } + + public class Department + { + public int id { get; set; } + public string name { get; set; } + } +} \ No newline at end of file diff --git a/src/SectigoCAProxy/API/OrganizationDetailsResponse.cs b/src/SectigoCAProxy/API/OrganizationDetailsResponse.cs new file mode 100644 index 0000000..cc9e097 --- /dev/null +++ b/src/SectigoCAProxy/API/OrganizationDetailsResponse.cs @@ -0,0 +1,29 @@ +// 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.AnyGateway.Sectigo.API +{ + public class OrganizationDetailsResponse + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("certTypes")] + public List CertTypes { get; set; } + } +} \ No newline at end of file diff --git a/src/SectigoCAProxy/Client/SectigoApiClient.cs b/src/SectigoCAProxy/Client/SectigoApiClient.cs index 60e2aea..420633f 100644 --- a/src/SectigoCAProxy/Client/SectigoApiClient.cs +++ b/src/SectigoCAProxy/Client/SectigoApiClient.cs @@ -1,13 +1,16 @@ -// 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 CSS.Common.Logging; -using Keyfactor.AnyGateway.Sectigo.API; +// 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 CSS.Common.Logging; + +using Keyfactor.AnyGateway.Sectigo.API; + using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -17,267 +20,296 @@ using System.Threading.Tasks; namespace Keyfactor.AnyGateway.Sectigo.Client -{ - public class SectigoApiClient : LoggingClientBase - { - HttpClient RestClient { get; } - public SectigoApiClient(HttpClient httpClient) - { - RestClient = httpClient; - } - public async Task GetCertificate(int sslId) - { - var response = await RestClient.GetAsync($"/api/ssl/v1/{sslId}"); - return await ProcessResponse(response); - } - public async Task CertificateListProducer(BlockingCollection certs, - CancellationToken cancelToken, int pageSize=25, string filter = "") - { - - int batchCount; - int blockedCount; - int totalCount = 0; - - List certificatePageToProcess; - try - { - //Paging loop will iterate though the certificates until all certificates have been returned - do - { - - if (cancelToken.IsCancellationRequested) - { - certs.CompleteAdding(); - break; - } - - int certIndex = totalCount > 0 ? (totalCount - 1) : 0; - Logger.Info($"Request Certificates at Position {certIndex} with Page Size {pageSize}"); - certificatePageToProcess = await PageCertificates(certIndex, pageSize, filter); - Logger.Debug($"Found {certificatePageToProcess.Count} certificate to process"); - - //Processing Loop will add and retry adding to queue until all certificates have been processed for a page - batchCount = 0; - blockedCount = 0; - do - { - Certificate cert = certificatePageToProcess[batchCount]; - Logger.Debug($"Processing: {cert}"); - Certificate certDetails = null; - try - { - if(certDetails==null) - certDetails = await GetCertificate(cert.Id); - } - catch (SectigoApiException aEx) - { - Logger.Error($"Error requesting certificate details. Skipping certificate. {aEx.Message}"); - batchCount++; - continue; - } - - if (certs.TryAdd(certDetails, 50, cancelToken)) - { - batchCount++; - totalCount++; - } - else - { - Logger.Trace($"Adding {cert.Id} to queue was blocked. Retry"); - blockedCount++;//TODO: If blocked count is excessive, should we skip? - } - certIndex++; - } - while (batchCount < certificatePageToProcess.Count); - - Logger.Info($"Added {batchCount} certificates to queue for processing."); - - } while (certificatePageToProcess.Count == pageSize);//if the API returns less than we requested, we assume we have reached the end - } - catch (HttpRequestException hEx) - { - Logger.Error($"Sync interrupted by HTTP Exception. {hEx.InnerException.Message}"); - certs.CompleteAdding();//Stops the consuming enumerable and sync will continue until the queue is empty - } - - catch (Exception ex) - { - //fail gracefully and stop syncing. - Logger.Error($"Sync interrupted by General Exception. {ex.Message}"); - certs.CompleteAdding();//Stops the consuming enumerable and sync will continue until the queue is empty - } - } - - public async Task CertificateListProducer(BlockingCollection certs, - CancellationToken cancelToken, int pageSize = 25, Dictionary filter = null) - { - if (filter != null && filter.Count > 0) - { - //each kvp key = type, value each filter - foreach (var s in filter) - { - foreach (var value in s.Value) - await CertificateListProducer(certs, cancelToken, pageSize, $"{s.Key}={value}"); - } - } - else +{ + public class SectigoApiClient : LoggingClientBase + { + private HttpClient RestClient { get; } + + public SectigoApiClient(HttpClient httpClient) + { + RestClient = httpClient; + } + + public async Task GetCertificate(int sslId) + { + var response = await RestClient.GetAsync($"/api/ssl/v1/{sslId}"); + return await ProcessResponse(response); + } + + public async Task CertificateListProducer(BlockingCollection certs, + CancellationToken cancelToken, int pageSize = 25, string filter = "") + { + int batchCount; + int blockedCount; + int totalCount = 0; + + List certificatePageToProcess; + try { - // No filters - await CertificateListProducer(certs, cancelToken, pageSize, ""); - } - - certs.CompleteAdding(); - } - - public async Task> PageCertificates(int position = 0, int size = 25, string filter = "") - { - string filterQueryString = String.IsNullOrEmpty(filter) ? string.Empty : $"&{filter}"; - Logger.Trace($"API Request: 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); - } - public async Task RevokeSslCertificateById(int sslId, string revreason) - { - JObject o = JObject.FromObject(new { - reason= revreason - }); - var response = await RestClient.PostAsJsonAsync($"api/ssl/v1/revoke/{sslId}", o); - if (response.IsSuccessStatusCode) - { - return true; - } - var failedResp = ProcessResponse(response).Result; - return failedResp.IsSuccess;//Should throw an exception with error message from API - } - public async Task ListOrganizations() - { - var response = await RestClient.GetAsync("api/organization/v1"); - var orgsResponse = await ProcessResponse>(response); - - return new ListOrganizationsResponse { Organizations = orgsResponse }; - } - public async Task ListPersons(int orgId) - { - int pageSize = 25; - List responseList = new List(); - List partialList = new List(); - do - { - partialList = await PagePerons(orgId, responseList.Count-1, pageSize); - responseList.AddRange(partialList); - } - while (partialList.Count == pageSize); - - return new ListPersonsResponse() { Persons = responseList }; - } - public async Task ListCustomFields() - { - var response = await RestClient.GetAsync("/api/ssl/v1/customFields"); - return new ListCustomFieldsResponse { CustomFields = await ProcessResponse>(response) }; - } - public async Task ListSslProfiles(int? orgId = null) - { - string urlSuffix=string.Empty; - if (orgId.HasValue) - { - urlSuffix = $"?organizationId={orgId}"; - } - - var response = await RestClient.GetAsync($"/api/ssl/v1/types{urlSuffix}"); - return new ListSslProfilesResponse { SslProfiles = await ProcessResponse>(response) }; - } - public async Task> PagePerons(int orgId, int position = 0, int size = 25) - { - var response = await RestClient.GetAsync($"api/person/v1?position={position}&size={size}&organizationId={orgId}"); - return await ProcessResponse>(response); - } - public async Task Enroll(EnrollRequest request) - { - try - { - var response = await RestClient.PostAsJsonAsync("/api/ssl/v1/enroll", request); - var enrollResponse = await ProcessResponse(response); - - return enrollResponse.sslId; - } - catch (InvalidOperationException invalidOp) - { - throw new Exception($"Invalid Operation. {invalidOp.Message}|{invalidOp.StackTrace}", invalidOp) ; - } - catch (HttpRequestException httpEx) - { - throw new Exception($"HttpRequestException. {httpEx.Message}|{httpEx.StackTrace}", httpEx); - } - catch (SectigoApiException) - { - throw; - } - } - 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 (SectigoApiException) - { - throw; - } - - } - public async Task PickupCertificate(int sslId, string subject) - { - var response = await RestClient.GetAsync($"/api/ssl/v1/collect/{sslId}/x509CO"); - - if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength > 0) - { - string pemChain = await response.Content.ReadAsStringAsync(); - - string[] splitChain = pemChain.Replace("\r\n", string.Empty).Split(new string[] { "-----" }, StringSplitOptions.RemoveEmptyEntries); - - return new X509Certificate2(Convert.FromBase64String(splitChain[1])); - } - return null; - //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 Func hexify = (ss => ss.Length <= 2 ? ss : ss.Substring(0, 2) + ":" + hexify(ss.Substring(2))); - - private static async Task ProcessResponse(HttpResponseMessage response) - { - if (response.IsSuccessStatusCode) - { - string responseContent = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(responseContent); - } - else - { - var error = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - throw new SectigoApiException($"{error.Code} | {error.Description}") { ErrorCode = error.Code, Description = error.Description}; - } - } - - private static string GetCertificateType(CertificateType type) - { - return Enum.GetName(typeof(CertificateType), type)?.ToLower(); - } - #endregion - } -} + //Paging loop will iterate though the certificates until all certificates have been returned + do + { + if (cancelToken.IsCancellationRequested) + { + certs.CompleteAdding(); + break; + } + + int certIndex = totalCount > 0 ? (totalCount - 1) : 0; + Logger.Info($"Request Certificates at Position {certIndex} with Page Size {pageSize}"); + certificatePageToProcess = await PageCertificates(certIndex, pageSize, filter); + Logger.Debug($"Found {certificatePageToProcess.Count} certificate to process"); + + //Processing Loop will add and retry adding to queue until all certificates have been processed for a page + batchCount = 0; + blockedCount = 0; + do + { + Certificate cert = certificatePageToProcess[batchCount]; + Logger.Debug($"Processing: {cert}"); + Certificate certDetails = null; + try + { + if (certDetails == null) + certDetails = await GetCertificate(cert.Id); + } + catch (SectigoApiException aEx) + { + Logger.Error($"Error requesting certificate details. Skipping certificate. {aEx.Message}"); + batchCount++; + continue; + } + + if (certs.TryAdd(certDetails, 50, cancelToken)) + { + batchCount++; + totalCount++; + } + else + { + Logger.Trace($"Adding {cert.Id} to queue was blocked. Retry"); + blockedCount++;//TODO: If blocked count is excessive, should we skip? + } + certIndex++; + } + while (batchCount < certificatePageToProcess.Count); + + Logger.Info($"Added {batchCount} certificates to queue for processing."); + } while (certificatePageToProcess.Count == pageSize);//if the API returns less than we requested, we assume we have reached the end + } + catch (HttpRequestException hEx) + { + Logger.Error($"Sync interrupted by HTTP Exception. {hEx.InnerException.Message}"); + certs.CompleteAdding();//Stops the consuming enumerable and sync will continue until the queue is empty + } + catch (Exception ex) + { + //fail gracefully and stop syncing. + Logger.Error($"Sync interrupted by General Exception. {ex.Message}"); + certs.CompleteAdding();//Stops the consuming enumerable and sync will continue until the queue is empty + } + } + + public async Task CertificateListProducer(BlockingCollection certs, + CancellationToken cancelToken, int pageSize = 25, Dictionary filter = null) + { + if (filter != null && filter.Count > 0) + { + //each kvp key = type, value each filter + foreach (var s in filter) + { + foreach (var value in s.Value) + await CertificateListProducer(certs, cancelToken, pageSize, $"{s.Key}={value}"); + } + } + else + { + // No filters + await CertificateListProducer(certs, cancelToken, pageSize, ""); + } + + certs.CompleteAdding(); + } + + public async Task> PageCertificates(int position = 0, int size = 25, string filter = "") + { + string filterQueryString = String.IsNullOrEmpty(filter) ? string.Empty : $"&{filter}"; + Logger.Trace($"API Request: 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); + } + + public async Task RevokeSslCertificateById(int sslId, string revreason) + { + JObject o = JObject.FromObject(new + { + reason = revreason + }); + var response = await RestClient.PostAsJsonAsync($"api/ssl/v1/revoke/{sslId}", o); + if (response.IsSuccessStatusCode) + { + return true; + } + var failedResp = ProcessResponse(response).Result; + return failedResp.IsSuccess;//Should throw an exception with error message from API + } + + public async Task ListOrganizations() + { + var response = await RestClient.GetAsync("api/organization/v1"); + if (response.IsSuccessStatusCode) + { + string responseContent = await response.Content.ReadAsStringAsync(); + Logger.Trace($"Raw Response: {responseContent}"); + } + var orgsResponse = await ProcessResponse>(response); + + return new ListOrganizationsResponse { Organizations = orgsResponse }; + } + + public async Task GetOrganizationDetails(int orgId) + { + var response = await RestClient.GetAsync($"api/organization/v1/{orgId}"); + if (response.IsSuccessStatusCode) + { + string responseContent = await response.Content.ReadAsStringAsync(); + Logger.Trace($"Raw Response: {responseContent}"); + } + + var orgDetailsResponse = await ProcessResponse(response); + return orgDetailsResponse; + } + + public async Task ListPersons(int orgId) + { + int pageSize = 25; + List responseList = new List(); + List partialList = new List(); + do + { + partialList = await PagePerons(orgId, responseList.Count - 1, pageSize); + responseList.AddRange(partialList); + } + while (partialList.Count == pageSize); + + return new ListPersonsResponse() { Persons = responseList }; + } + + public async Task ListCustomFields() + { + var response = await RestClient.GetAsync("/api/ssl/v1/customFields"); + return new ListCustomFieldsResponse { CustomFields = await ProcessResponse>(response) }; + } + + public async Task ListSslProfiles(int? orgId = null) + { + string urlSuffix = string.Empty; + if (orgId.HasValue) + { + urlSuffix = $"?organizationId={orgId}"; + } + + var response = await RestClient.GetAsync($"/api/ssl/v1/types{urlSuffix}"); + return new ListSslProfilesResponse { SslProfiles = await ProcessResponse>(response) }; + } + + public async Task> PagePerons(int orgId, int position = 0, int size = 25) + { + var response = await RestClient.GetAsync($"api/person/v1?position={position}&size={size}&organizationId={orgId}"); + return await ProcessResponse>(response); + } + + public async Task Enroll(EnrollRequest request) + { + try + { + var response = await RestClient.PostAsJsonAsync("/api/ssl/v1/enroll", request); + var enrollResponse = await ProcessResponse(response); + + return enrollResponse.sslId; + } + catch (InvalidOperationException invalidOp) + { + throw new Exception($"Invalid Operation. {invalidOp.Message}|{invalidOp.StackTrace}", invalidOp); + } + catch (HttpRequestException httpEx) + { + throw new Exception($"HttpRequestException. {httpEx.Message}|{httpEx.StackTrace}", httpEx); + } + catch (SectigoApiException) + { + throw; + } + } + + 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 (SectigoApiException) + { + throw; + } + } + + public async Task PickupCertificate(int sslId, string subject) + { + var response = await RestClient.GetAsync($"/api/ssl/v1/collect/{sslId}/x509CO"); + + if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength > 0) + { + string pemChain = await response.Content.ReadAsStringAsync(); + + string[] splitChain = pemChain.Replace("\r\n", string.Empty).Split(new string[] { "-----" }, StringSplitOptions.RemoveEmptyEntries); + + return new X509Certificate2(Convert.FromBase64String(splitChain[1])); + } + return null; + //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 Func hexify = (ss => ss.Length <= 2 ? ss : ss.Substring(0, 2) + ":" + hexify(ss.Substring(2))); + + private static async Task ProcessResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + string responseContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseContent); + } + else + { + var error = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + throw new SectigoApiException($"{error.Code} | {error.Description}") { ErrorCode = error.Code, Description = error.Description }; + } + } + + private static string GetCertificateType(CertificateType type) + { + return Enum.GetName(typeof(CertificateType), type)?.ToLower(); + } + + #endregion Static Methods + } +} \ No newline at end of file diff --git a/src/SectigoCAProxy/SectigoCAProxy.cs b/src/SectigoCAProxy/SectigoCAProxy.cs index c779ef2..e8cad7c 100644 --- a/src/SectigoCAProxy/SectigoCAProxy.cs +++ b/src/SectigoCAProxy/SectigoCAProxy.cs @@ -272,7 +272,7 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe } Logger.Debug($"Search for Organization by Name {orgStr}"); - int requstOrgId = 0; + int requestOrgId = 0; var org = Task.Run(async () => await GetOrganizationAsync(orgStr)).Result; if (org == null) { @@ -281,22 +281,18 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe return new EnrollmentResult { Status = 30, StatusMessage = err }; } - Department dep = null; - //API returned no CertType node or it was an empty string. This changed at some point with the Sectigo API. - if (org.certTypes == null || org.certTypes.Count == 0) + if (!string.IsNullOrEmpty(department)) { - Logger.Trace($"{orgStr} does not contain a valid certificate type configuration. Checking department."); - if (string.IsNullOrEmpty(department)) + // If department is specified in the config for this product type, look up the department configuration + + if (org.departments == null || org.departments.Count == 0) { - string err = $"No department specified, and organization {orgStr} does not contain valid certificate type configuration. Verify account and gateway configuration."; + string err = $"Department {department} not found: no departments found in organization {orgStr}"; Logger.Error($"{err}"); - if (!string.IsNullOrEmpty(ouStr)) - { - Logger.Error("NOTE: Organizational Unit subject field has been deprecated. Department names must now be specified in the gateway template configuration. See documentation for details."); - } return new EnrollmentResult { Status = 30, StatusMessage = err }; } - dep = org.departments.Where(x => x.name.ToLower().Equals(department.ToLower())).FirstOrDefault(); + + Department dep = org.departments.Where(d => d.name.Equals(department, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (dep == null) { @@ -305,13 +301,40 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe return new EnrollmentResult { Status = 30, StatusMessage = err }; } - Logger.Trace($"{dep} is valid. Apply {dep.id} to request"); - requstOrgId = dep.id; + Logger.Debug($"Retrieving details of department {department}"); + + var orgDetails = Task.Run(async () => await Client.GetOrganizationDetails(dep.id)).Result; + + if (orgDetails.CertTypes == null || orgDetails.CertTypes.Count == 0) + { + string err = $"Department {department} does not contain a valid certificate type configuration. Please verify account configuration."; + Logger.Error($"{err}"); + return new EnrollmentResult { Status = 30, StatusMessage = err }; + } + + Logger.Debug($"Department {dep.name} is valid. Using ID {dep.id} for request"); + requestOrgId = dep.id; } else { - Logger.Trace($"{orgStr} contain a valid certificate type configuration. Apply {org.id} to request"); - requstOrgId = org.id; + // If no department is specified, look up the config of the organization itself + + Logger.Debug($"Retrieving details of organization {orgStr}"); + var orgDetails = Task.Run(async () => await Client.GetOrganizationDetails(org.id)).Result; + + if (orgDetails.CertTypes == null || orgDetails.CertTypes.Count == 0) + { + string err = $"Organization {orgStr} does not contain a valid certificate type configuration, and no department was specified.Please verify account configuration."; + Logger.Error($"{err}"); + if (!string.IsNullOrEmpty(ouStr)) + { + Logger.Error("NOTE: Organizational Unit subject field has been deprecated. Department names must now be specified in the gateway template configuration. See documentation for details."); + } + return new EnrollmentResult { Status = 30, StatusMessage = err }; + } + + Logger.Debug($"Organization {org.name} is valid. Using ID {org.id} for request"); + requestOrgId = org.id; } //Check if SAN matches the SUBJECT CN when multidomain = false (single domain cert). @@ -338,7 +361,7 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe EnrollRequest request = new EnrollRequest { csr = csr, - orgId = requstOrgId, + orgId = requestOrgId, term = Task.Run(async () => await GetProfileTerm(int.Parse(productInfo.ProductID))).Result, certType = enrollmentProfile.id, //External requestor is expected to be an email. Use config to pull the enrollment field or send blank diff --git a/src/SectigoCAProxy/SectigoCAProxy.csproj b/src/SectigoCAProxy/SectigoCAProxy.csproj index 01cdf38..1a35de2 100644 --- a/src/SectigoCAProxy/SectigoCAProxy.csproj +++ b/src/SectigoCAProxy/SectigoCAProxy.csproj @@ -1,120 +1,121 @@ - - - - - Debug - AnyCPU - {EA70088A-23B7-4753-B764-8DDEC8571522} - Library - Properties - Keyfactor.AnyGateway.Sectigo - SectigoCAProxy - v4.6.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - x64 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - - - - ..\..\packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll - - - ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.AnyGateway.Core.dll - - - ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.Interfaces.dll - - - ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxyDAL.dll - - - ..\..\packages\Common.Logging.3.4.1\lib\net40\Common.Logging.dll - - - ..\..\packages\Common.Logging.Core.3.4.1\lib\net40\Common.Logging.Core.dll - - - ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CommonCAProxy.dll - - - ..\..\packages\CSS.Common.1.6.0\lib\net462\CSS.Common.dll - - - ..\..\packages\CSS.PKI.2.13.0\lib\net462\CSS.PKI.dll - - - ..\..\packages\CERTENROLLLibx64.1.0.0.1\lib\net35\Interop.CERTENROLLLib.dll - True - - - ..\..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll - - - - - ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll - - - ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll - - - ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + Debug + AnyCPU + {EA70088A-23B7-4753-B764-8DDEC8571522} + Library + Properties + Keyfactor.AnyGateway.Sectigo + SectigoCAProxy + v4.6.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x64 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + + ..\..\packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll + + + ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.AnyGateway.Core.dll + + + ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.Interfaces.dll + + + ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxyDAL.dll + + + ..\..\packages\Common.Logging.3.4.1\lib\net40\Common.Logging.dll + + + ..\..\packages\Common.Logging.Core.3.4.1\lib\net40\Common.Logging.Core.dll + + + ..\..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CommonCAProxy.dll + + + ..\..\packages\CSS.Common.1.6.0\lib\net462\CSS.Common.dll + + + ..\..\packages\CSS.PKI.2.13.0\lib\net462\CSS.PKI.dll + + + ..\..\packages\CERTENROLLLibx64.1.0.0.1\lib\net35\Interop.CERTENROLLLib.dll + True + + + ..\..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + + + + ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll + + + ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file