diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31593af35..ae0bfa4d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' @@ -44,7 +44,7 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 46df41e77..81637be17 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -73,7 +73,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' @@ -94,6 +94,6 @@ jobs: run: mvn -B -U verify - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/README.md b/README.md index 12112f459..040695cf1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CZERTAINLY Core -> This repository is part of the commercial open source project CZERTAINLY. You can find more information about the project at [CZERTAINLY](https://github.com/3KeyCompany/CZERTAINLY) repository, including the contribution guide. +> This repository is part of the open source project CZERTAINLY. You can find more information about the project at [CZERTAINLY](https://github.com/3KeyCompany/CZERTAINLY) repository, including the contribution guide. `Core` provides the basic functionality for the CZERTAINLY platform. It implements the logic for the certificate lifecycle management and handles all related tasks. You can think about it as a brain of the CZERTAINLY platform. diff --git a/hooks/build b/hooks/build old mode 100644 new mode 100755 index 0ef5f8487..4c8d2730e --- a/hooks/build +++ b/hooks/build @@ -14,7 +14,12 @@ echo "PreBuild build" docker build --build-arg SERVER_USERNAME=$SERVER_USERNAME --build-arg SERVER_PASSWORD=$SERVER_PASSWORD -f $DOCKERFILE_PATH-pre -t prebuild . echo "MVN Build" -docker run -v /var/run/docker.sock:/var/run/docker.sock --name czertainlycont -i prebuild mvn -f /home/app/pom.xml clean package +if [[ "$OSTYPE" == "darwin"* ]]; then + echo "MacOS detected, using TESTCONTAINERS_HOST_OVERRIDE" + docker run -e TESTCONTAINERS_HOST_OVERRIDE=docker.for.mac.host.internal -v /var/run/docker.sock:/var/run/docker.sock --name czertainlycont -i prebuild mvn -f /home/app/pom.xml clean package +else + docker run -v /var/run/docker.sock:/var/run/docker.sock --name czertainlycont -i prebuild mvn -f /home/app/pom.xml clean package +fi echo "Starting czertainlycont" docker start -d --restart=always czertainlycont diff --git a/pom.xml b/pom.xml index 737a32ba6..ba1a3088b 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ core - 2.10.0 + 2.11.0 CZERTAINLY-Core @@ -26,7 +26,7 @@ com.czertainly interfaces - 2.10.0 + 2.11.0 @@ -164,7 +164,7 @@ com.microsoft.azure msal4j - 1.14.0 + 1.14.2 org.apache.httpcomponents diff --git a/settings.xml b/settings.xml index 431b741ed..a29599d18 100644 --- a/settings.xml +++ b/settings.xml @@ -12,8 +12,8 @@ github - central - https://repo1.maven.org/maven2 + ossrh-releases + https://s01.oss.sonatype.org/content/repositories/releases github diff --git a/src/main/java/com/czertainly/core/api/ExceptionHandlingAdvice.java b/src/main/java/com/czertainly/core/api/ExceptionHandlingAdvice.java index d1531b8c6..c4bb0e190 100644 --- a/src/main/java/com/czertainly/core/api/ExceptionHandlingAdvice.java +++ b/src/main/java/com/czertainly/core/api/ExceptionHandlingAdvice.java @@ -14,11 +14,13 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingRequestValueException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import java.net.ConnectException; import java.security.cert.CertificateException; @@ -78,6 +80,42 @@ public ErrorMessageDto handleHttpRequestMethodNotSupportedException(HttpRequestM return ErrorMessageDto.getInstance(ex.getMessage()); } + /** + * Handler for {@link IllegalArgumentException}. + * + * @return + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessageDto handleIllegalArgumentException(IllegalArgumentException ex) { + LOG.info("HTTP 400: {}", ex.getMessage()); + return ErrorMessageDto.getInstance(ex.getMessage()); + } + + /** + * Handler for {@link MethodArgumentTypeMismatchException}. + * + * @return + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessageDto handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) { + LOG.info("HTTP 400: {}", ex.getMessage()); + return ErrorMessageDto.getInstance(ex.getMessage()); + } + + /** + * Handler for {@link MissingRequestValueException}. + * + * @return + */ + @ExceptionHandler(MissingRequestValueException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessageDto handleMissingRequestValueException(MissingRequestValueException ex) { + LOG.info("HTTP 400: {}", ex.getMessage()); + return ErrorMessageDto.getInstance(ex.getMessage()); + } + /** * Handler for {@link NotDeletableException}. * diff --git a/src/main/java/com/czertainly/core/api/web/CertificateControllerImpl.java b/src/main/java/com/czertainly/core/api/web/CertificateControllerImpl.java index f86a8fdd9..0d1ebc3d3 100644 --- a/src/main/java/com/czertainly/core/api/web/CertificateControllerImpl.java +++ b/src/main/java/com/czertainly/core/api/web/CertificateControllerImpl.java @@ -22,6 +22,7 @@ import com.czertainly.core.service.CertificateService; import com.czertainly.core.service.v2.ClientOperationService; import com.czertainly.core.util.converter.CertificateFormatConverter; +import com.czertainly.core.util.converter.CertificateFormatEncodingConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; @@ -55,6 +56,7 @@ public class CertificateControllerImpl implements CertificateController { @InitBinder public void initBinder(final WebDataBinder webdataBinder) { webdataBinder.registerCustomEditor(CertificateFormat.class, new CertificateFormatConverter()); + webdataBinder.registerCustomEditor(CertificateFormatEncoding.class, new CertificateFormatEncodingConverter()); } @Override @@ -68,6 +70,11 @@ public CertificateDetailDto getCertificate(@PathVariable String uuid) return certificateService.getCertificate(SecuredUUID.fromString(uuid)); } + @Override + public CertificateDownloadResponseDto downloadCertificate(String uuid, CertificateFormat certificateFormat, CertificateFormatEncoding encoding) throws CertificateException, NotFoundException, IOException { + return certificateService.downloadCertificate(uuid, certificateFormat, encoding); + } + @Override public void deleteCertificate(@PathVariable String uuid) throws NotFoundException { certificateService.deleteCertificate(SecuredUUID.fromString(uuid)); @@ -153,8 +160,8 @@ public CertificateChainResponseDto getCertificateChain(String uuid, boolean with } @Override - public CertificateChainDownloadResponseDto downloadCertificateChain(String uuid, CertificateFormat certificateFormat, boolean withEndCertificate) throws NotFoundException, CertificateException { - return certificateService.downloadCertificateChain(SecuredUUID.fromString(uuid), certificateFormat, withEndCertificate); + public CertificateChainDownloadResponseDto downloadCertificateChain(String uuid, CertificateFormat certificateFormat, boolean withEndCertificate, CertificateFormatEncoding encoding) throws NotFoundException, CertificateException { + return certificateService.downloadCertificateChain(SecuredUUID.fromString(uuid), certificateFormat, withEndCertificate, encoding); } @Override diff --git a/src/main/java/com/czertainly/core/api/web/RAProfileManagementControllerImpl.java b/src/main/java/com/czertainly/core/api/web/RAProfileManagementControllerImpl.java index df382e228..599f0e42b 100644 --- a/src/main/java/com/czertainly/core/api/web/RAProfileManagementControllerImpl.java +++ b/src/main/java/com/czertainly/core/api/web/RAProfileManagementControllerImpl.java @@ -11,6 +11,7 @@ import com.czertainly.api.model.common.UuidDto; import com.czertainly.api.model.common.attribute.v2.BaseAttribute; import com.czertainly.api.model.core.auth.Resource; +import com.czertainly.api.model.core.certificate.CertificateDetailDto; import com.czertainly.api.model.core.raprofile.RaProfileDto; import com.czertainly.core.auth.AuthEndpoint; import com.czertainly.core.security.authz.SecuredParentUUID; @@ -23,6 +24,8 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.List; import java.util.Optional; @@ -164,4 +167,10 @@ public void associateRAProfileWithApprovalProfile(String authorityUuid, String r public void disassociateRAProfileFromApprovalProfile(String authorityUuid, String raProfileUuid, String approvalProfileUuid) throws NotFoundException { raProfileService.disassociateApprovalProfile(authorityUuid, raProfileUuid, SecuredUUID.fromString(approvalProfileUuid)); } + + @Override + public List getAuthorityCertificateChain(String authorityUuid, String raProfileUuid) throws ConnectorException { + return raProfileService.getAuthorityCertificateChain(SecuredParentUUID.fromString(authorityUuid), SecuredUUID.fromString(raProfileUuid)); + } + } diff --git a/src/main/java/com/czertainly/core/dao/entity/Certificate.java b/src/main/java/com/czertainly/core/dao/entity/Certificate.java index 842e2cd01..fa016429c 100644 --- a/src/main/java/com/czertainly/core/dao/entity/Certificate.java +++ b/src/main/java/com/czertainly/core/dao/entity/Certificate.java @@ -182,6 +182,9 @@ public class Certificate extends UniquelyIdentifiedAndAudited implements Seriali @Column(name = "revoke_attributes") private String revokeAttributes; + @Column(name = "trusted_ca") + private Boolean trustedCa; + @Override public CertificateDetailDto mapToDto() { final CertificateDetailDto dto = new CertificateDetailDto(); @@ -210,6 +213,7 @@ public CertificateDetailDto mapToDto() { dto.setValidationStatus(validationStatus); dto.setCertificateType(certificateType); dto.setOwner(owner); + dto.setTrustedCa(trustedCa); if (issuerCertificateUuid != null) dto.setIssuerCertificateUuid(issuerCertificateUuid.toString()); if (ownerUuid != null) dto.setOwnerUuid(ownerUuid.toString()); @@ -293,6 +297,7 @@ public CertificateDto mapToListDto() { dto.setValidationStatus(validationStatus); dto.setFingerprint(fingerprint); dto.setOwner(owner); + dto.setTrustedCa(trustedCa); if (issuerCertificateUuid != null) dto.setIssuerCertificateUuid(issuerCertificateUuid.toString()); if (ownerUuid != null) dto.setOwnerUuid(ownerUuid.toString()); dto.setCertificateType(certificateType); @@ -744,4 +749,12 @@ private String getIssuerCommonNameToDto() { } return null; } + + public void setTrustedCa(boolean trustedCa) { + this.trustedCa = trustedCa; + } + + public Boolean getTrustedCa() { + return trustedCa; + } } diff --git a/src/main/java/com/czertainly/core/dao/entity/Crl.java b/src/main/java/com/czertainly/core/dao/entity/Crl.java new file mode 100644 index 000000000..d1b785a00 --- /dev/null +++ b/src/main/java/com/czertainly/core/dao/entity/Crl.java @@ -0,0 +1,53 @@ +package com.czertainly.core.dao.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Getter +@Setter +@Entity +@Table(name = "crl") +public class Crl extends UniquelyIdentified { + @Column(name = "ca_certificate_uuid") + private UUID caCertificateUuid; + + @Column(name = "issuer_dn", nullable = false) + private String issuerDn; + + @Column(name = "serial_number", nullable = false) + private String serialNumber; + + @Column(name = "crl_issuer_dn", nullable = false) + private String crlIssuerDn; + + @Column(name = "crl_number", nullable = false) + private String crlNumber; + + @Column(name = "next_update", nullable = false) + private Date nextUpdate; + + @Column(name = "crl_number_delta") + private String crlNumberDelta; + + @Column(name = "next_update_delta") + private Date nextUpdateDelta; + + @Column(name = "last_revocation_date") + private Date lastRevocationDate; + + @OneToMany(mappedBy = "crl", fetch = FetchType.LAZY) + @JsonBackReference + private List crlEntries; + + public Map getCrlEntriesMap() { + return crlEntries.stream().collect(Collectors.toMap(crlEntry -> crlEntry.getId().getSerialNumber(), crlEntry -> crlEntry)); + } +} diff --git a/src/main/java/com/czertainly/core/dao/entity/CrlEntry.java b/src/main/java/com/czertainly/core/dao/entity/CrlEntry.java new file mode 100644 index 000000000..8fe7b0dbc --- /dev/null +++ b/src/main/java/com/czertainly/core/dao/entity/CrlEntry.java @@ -0,0 +1,64 @@ +package com.czertainly.core.dao.entity; + +import com.czertainly.api.model.core.authority.CertificateRevocationReason; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; + +@Getter +@Setter +@Entity +@Table(name = "crl_entry") +public class CrlEntry implements Serializable { + + @EmbeddedId + private CrlEntryId id = new CrlEntryId(); + + @ManyToOne + @MapsId("crlUuid") + private Crl crl; + + @Column(name = "revocation_date", nullable = false) + private Date revocationDate; + + @Column(name = "revocation_reason", nullable = false) + @Enumerated(EnumType.STRING) + private CertificateRevocationReason revocationReason; + + public UUID getCrlUuid() { + return crl.getUuid(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) + return false; + + CrlEntry that = (CrlEntry) o; + return Objects.equals(crl, that.crl) && + Objects.equals(id.getSerialNumber(), that.getId().getSerialNumber()); + } + + @Override + public int hashCode() { + return Objects.hash(getCrlUuid(), id.getSerialNumber()); + } + + @Override + public String toString() { + return "CertificateLocation{" + + "id=" + id + + ", revocationReason='" + revocationReason + '\'' + + ", revocationDate='" + revocationDate + '\'' + + '}'; + } + + +} diff --git a/src/main/java/com/czertainly/core/dao/entity/CrlEntryId.java b/src/main/java/com/czertainly/core/dao/entity/CrlEntryId.java new file mode 100644 index 000000000..b1db896be --- /dev/null +++ b/src/main/java/com/czertainly/core/dao/entity/CrlEntryId.java @@ -0,0 +1,60 @@ +package com.czertainly.core.dao.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; + +/** + * Embedded class for a composite primary key + */ +@Getter +@Setter +@Embeddable +public class CrlEntryId implements Serializable { + + @Column(name = "crl_uuid", nullable = false) + private UUID crlUuid; + + @Column(name = "serial_number", nullable = false) + private String serialNumber; + + public CrlEntryId() { + } + + public CrlEntryId(UUID crlUuid, String serialNumber) { + this.crlUuid = crlUuid; + this.serialNumber = serialNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) + return false; + + CrlEntryId that = (CrlEntryId) o; + return Objects.equals(crlUuid, that.crlUuid) && + Objects.equals(serialNumber, that.serialNumber); + } + + @Override + public int hashCode() { + return Objects.hash(crlUuid, serialNumber); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("crlUuid", crlUuid) + .append("serialNumber", serialNumber) + .toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/czertainly/core/dao/entity/RaProfile.java b/src/main/java/com/czertainly/core/dao/entity/RaProfile.java index 8a40ec1a6..4f34b432b 100644 --- a/src/main/java/com/czertainly/core/dao/entity/RaProfile.java +++ b/src/main/java/com/czertainly/core/dao/entity/RaProfile.java @@ -54,6 +54,9 @@ public class RaProfile extends UniquelyIdentifiedAndAudited implements Serializa @Column(name = "enabled") private Boolean enabled; + @Column(name = "authority_certificate_uuid") + private UUID authorityCertificateUuid; + @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinTable( name = "ra_profile_2_compliance_profile", @@ -274,6 +277,14 @@ public void setProtocolAttribute(RaProfileProtocolAttribute protocolAttribute) { this.protocolAttribute = protocolAttribute; } + public UUID getAuthorityCertificateUuid() { + return authorityCertificateUuid; + } + + public void setAuthorityCertificateUuid(UUID authorityCertificateUuid) { + this.authorityCertificateUuid = authorityCertificateUuid; + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) diff --git a/src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java b/src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java index 259279f42..7b7a4aeb5 100644 --- a/src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java +++ b/src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java @@ -30,8 +30,6 @@ public interface CertificateRepository extends SecurityFilterRepository findBySubjectDn(String subjectDn); - List findByRaProfile(RaProfile raProfile); List findByGroup(Group group); @@ -76,7 +74,7 @@ List findCertificatesToCheckStatus(@Param("statusValidityEndTimesta List findByRaProfileAndComplianceStatusIsNotNull(RaProfile raProfile); - Optional findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber); + Optional findBySubjectDnNormalizedAndSerialNumber(String subjectDnNormalized, String serialNumber); Optional findByIssuerDnNormalizedAndSerialNumber(String issuerDnNormalized, String serialNumber); diff --git a/src/main/java/com/czertainly/core/dao/repository/CrlEntryRepository.java b/src/main/java/com/czertainly/core/dao/repository/CrlEntryRepository.java new file mode 100644 index 000000000..3a50e99e8 --- /dev/null +++ b/src/main/java/com/czertainly/core/dao/repository/CrlEntryRepository.java @@ -0,0 +1,14 @@ +package com.czertainly.core.dao.repository; + +import com.czertainly.core.dao.entity.CrlEntry; +import com.czertainly.core.dao.entity.CrlEntryId; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CrlEntryRepository extends SecurityFilterRepository +{ + + Optional findById(CrlEntryId id); +} diff --git a/src/main/java/com/czertainly/core/dao/repository/CrlRepository.java b/src/main/java/com/czertainly/core/dao/repository/CrlRepository.java new file mode 100644 index 000000000..756068292 --- /dev/null +++ b/src/main/java/com/czertainly/core/dao/repository/CrlRepository.java @@ -0,0 +1,14 @@ +package com.czertainly.core.dao.repository; + +import com.czertainly.core.dao.entity.Crl; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CrlRepository extends SecurityFilterRepository +{ + Optional findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber); + + +} diff --git a/src/main/java/com/czertainly/core/enums/SearchFieldNameEnum.java b/src/main/java/com/czertainly/core/enums/SearchFieldNameEnum.java index 12ff0ca01..fbcb5a59f 100644 --- a/src/main/java/com/czertainly/core/enums/SearchFieldNameEnum.java +++ b/src/main/java/com/czertainly/core/enums/SearchFieldNameEnum.java @@ -55,7 +55,9 @@ public enum SearchFieldNameEnum { LOCATION_INSTANCE_NAME(SearchableFields.ENTITY_INSTANCE_NAME, "Entity instance", SearchFieldTypeEnum.LIST), LOCATION_ENABLED(SearchableFields.ENABLED, "Enabled", SearchFieldTypeEnum.BOOLEAN), LOCATION_SUPPORT_MULTIPLE_ENTRIES(SearchableFields.SUPPORT_MULTIPLE_ENTRIES, "Support multiple entries", SearchFieldTypeEnum.BOOLEAN), - LOCATION_SUPPORT_KEY_MANAGEMENT(SearchableFields.SUPPORT_KEY_MANAGEMENT, "Support key management", SearchFieldTypeEnum.BOOLEAN); + LOCATION_SUPPORT_KEY_MANAGEMENT(SearchableFields.SUPPORT_KEY_MANAGEMENT, "Support key management", SearchFieldTypeEnum.BOOLEAN), + + TRUSTED_CA(SearchableFields.TRUSTED_CA, "Trusted CA", SearchFieldTypeEnum.BOOLEAN); private SearchableFields fieldProperty; diff --git a/src/main/java/com/czertainly/core/messaging/listeners/NotificationListener.java b/src/main/java/com/czertainly/core/messaging/listeners/NotificationListener.java index 57405038a..adc84e23b 100644 --- a/src/main/java/com/czertainly/core/messaging/listeners/NotificationListener.java +++ b/src/main/java/com/czertainly/core/messaging/listeners/NotificationListener.java @@ -180,9 +180,14 @@ private void sendExternalNotifications(UUID notificationInstanceUUID, Notificati UUID roleUuid = recipient.getRecipientUuid(); try { RoleDetailDto roleDetailDto = roleManagementApiClient.getRoleDetail(roleUuid.toString()); - recipientDto = new NotificationRecipientDto(); - recipientDto.setEmail(roleDetailDto.getEmail()); - recipientDto.setName(roleDetailDto.getName()); + String email = roleDetailDto.getEmail(); + if (email == null || email.isBlank()) { + logger.warn("Role with UUID {} does not have specified email, notification was not sent for this role.", roleUuid); + } else { + recipientDto = new NotificationRecipientDto(); + recipientDto.setEmail(email); + recipientDto.setName(roleDetailDto.getName()); + } recipientCustomAttributes = attributeService.getCustomAttributesWithValues(roleUuid, Resource.ROLE); } catch (Exception e) { @@ -193,9 +198,14 @@ private void sendExternalNotifications(UUID notificationInstanceUUID, Notificati UUID groupUuid = recipient.getRecipientUuid(); Optional group = groupRepository.findByUuid(groupUuid); if (group.isPresent()) { - NotificationRecipientDto groupDto = new NotificationRecipientDto(); - groupDto.setName(group.get().getName()); - groupDto.setEmail(group.get().getEmail()); + String email = group.get().getEmail(); + if (email == null || email.isBlank()) { + logger.warn("Group with UUID {} does not have specified email, notification was not sent for this group.", groupUuid); + } else { + recipientDto = new NotificationRecipientDto(); + recipientDto.setName(group.get().getName()); + recipientDto.setEmail(email); + } recipientCustomAttributes = attributeService.getCustomAttributesWithValues(groupUuid, Resource.GROUP); } else { diff --git a/src/main/java/com/czertainly/core/messaging/producers/NotificationProducer.java b/src/main/java/com/czertainly/core/messaging/producers/NotificationProducer.java index 6bc2bbaf7..1b718cdff 100644 --- a/src/main/java/com/czertainly/core/messaging/producers/NotificationProducer.java +++ b/src/main/java/com/czertainly/core/messaging/producers/NotificationProducer.java @@ -63,7 +63,7 @@ public void produceNotificationCertificateStatusChanged(CertificateValidationSta produceMessage(new NotificationMessage(NotificationType.CERTIFICATE_STATUS_CHANGED, Resource.CERTIFICATE, UUID.fromString(certificateDto.getUuid()), - NotificationRecipient.buildUserOrGroupNotificationRecipient(UUID.fromString(certificateDto.getOwnerUuid()), certificateDto.getGroup() != null ? UUID.fromString(certificateDto.getGroup().getUuid()) : null), + NotificationRecipient.buildUserOrGroupNotificationRecipient(certificateDto.getOwnerUuid() != null ? UUID.fromString(certificateDto.getOwnerUuid()) : null, certificateDto.getGroup() != null ? UUID.fromString(certificateDto.getGroup().getUuid()) : null), certificateDto.getRaProfile() == null ? new NotificationDataCertificateStatusChanged(oldStatus.getLabel(), newStatus.getLabel(), certificateDto.getUuid(), certificateDto.getFingerprint(), certificateDto.getSerialNumber(), certificateDto.getSubjectDn(), certificateDto.getIssuerDn()) : new NotificationDataCertificateStatusChanged(oldStatus.getLabel(), newStatus.getLabel(), certificateDto.getUuid(), certificateDto.getFingerprint(), certificateDto.getSerialNumber(), certificateDto.getSubjectDn(), certificateDto.getIssuerDn(), certificateDto.getRaProfile().getAuthorityInstanceUuid(), certificateDto.getRaProfile().getUuid(), certificateDto.getRaProfile().getName()))); } diff --git a/src/main/java/com/czertainly/core/service/CertificateService.java b/src/main/java/com/czertainly/core/service/CertificateService.java index 71e60924a..0e88a2dbb 100644 --- a/src/main/java/com/czertainly/core/service/CertificateService.java +++ b/src/main/java/com/czertainly/core/service/CertificateService.java @@ -51,7 +51,9 @@ public interface CertificateService extends ResourceExtensionService { CertificateChainResponseDto getCertificateChain(SecuredUUID uuid, boolean withEndCertificate) throws NotFoundException; - CertificateChainDownloadResponseDto downloadCertificateChain(SecuredUUID uuid, CertificateFormat certificateFormat, boolean withEndCertificate) throws NotFoundException, CertificateException; + CertificateChainDownloadResponseDto downloadCertificateChain(SecuredUUID uuid, CertificateFormat certificateFormat, boolean withEndCertificate, CertificateFormatEncoding encoding) throws NotFoundException, CertificateException; + + CertificateDownloadResponseDto downloadCertificate(String uuid, CertificateFormat certificateFormat, CertificateFormatEncoding encoding) throws CertificateException, NotFoundException, IOException; /** * Function to get the validation result of the certificate diff --git a/src/main/java/com/czertainly/core/service/CrlService.java b/src/main/java/com/czertainly/core/service/CrlService.java new file mode 100644 index 000000000..a6ce7141a --- /dev/null +++ b/src/main/java/com/czertainly/core/service/CrlService.java @@ -0,0 +1,19 @@ +package com.czertainly.core.service; + +import com.czertainly.core.dao.entity.Crl; +import com.czertainly.core.dao.entity.CrlEntry; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.UUID; + +public interface CrlService { + + Crl createCrlAndCrlEntries(byte[] crlDistributionPointsEncoded, String issuerDn, String issuerSerialNumber, UUID caCertificateUuid, String oldCrlNumber) throws IOException; + + Crl updateCrlAndCrlEntriesFromDeltaCrl(X509Certificate certificate, Crl crl, String issuerDn, String issuerSerialNumber, UUID caCertificateUuid) throws IOException; + + Crl getCurrentCrl(X509Certificate certificate, X509Certificate issuerCertificate) throws IOException; + + CrlEntry findCrlEntryForCertificate(String serialNumber, UUID crlUuid); +} diff --git a/src/main/java/com/czertainly/core/service/RaProfileService.java b/src/main/java/com/czertainly/core/service/RaProfileService.java index 646b519a5..158ca170c 100644 --- a/src/main/java/com/czertainly/core/service/RaProfileService.java +++ b/src/main/java/com/czertainly/core/service/RaProfileService.java @@ -9,6 +9,7 @@ import com.czertainly.api.model.client.compliance.SimplifiedComplianceProfileDto; import com.czertainly.api.model.client.raprofile.*; import com.czertainly.api.model.common.attribute.v2.BaseAttribute; +import com.czertainly.api.model.core.certificate.CertificateDetailDto; import com.czertainly.api.model.core.raprofile.RaProfileDto; import com.czertainly.core.dao.entity.RaProfile; import com.czertainly.core.security.authz.SecuredParentUUID; @@ -16,6 +17,8 @@ import com.czertainly.core.security.authz.SecurityFilter; import com.czertainly.core.service.model.SecuredList; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.List; import java.util.Optional; @@ -121,4 +124,16 @@ public interface RaProfileService extends ResourceExtensionService { ApprovalProfileRelationDto associateApprovalProfile(String authorityInstanceUuid, String raProfileUuid, SecuredUUID approvalProfileUuid) throws NotFoundException; void disassociateApprovalProfile(String authorityInstanceUuid, String raProfileUuid, SecuredUUID approvalProfileUuid) throws NotFoundException; + + /** + * Function to get the list of CA certificates associated with the RA Profile. + * Certificate chain is returned from the connector if the endpoint is implemented in the connector. + * + * @param authorityUuid UUID of the authority + * @param raProfileUuid UUID of the RA Profile + * @return List of CA Certificates + * @throws ConnectorException in case the connector throws an exception + */ + List getAuthorityCertificateChain(SecuredParentUUID authorityUuid, SecuredUUID raProfileUuid) + throws ConnectorException; } diff --git a/src/main/java/com/czertainly/core/service/impl/AttributeServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/AttributeServiceImpl.java index 31c269524..9120d267f 100644 --- a/src/main/java/com/czertainly/core/service/impl/AttributeServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/AttributeServiceImpl.java @@ -41,6 +41,8 @@ import com.czertainly.core.util.AttributeDefinitionUtils; import com.czertainly.core.util.SearchHelper; import com.czertainly.core.util.converter.Sql2PredicateConverter; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; @@ -56,6 +58,8 @@ @Service @Transactional public class AttributeServiceImpl implements AttributeService { + private static final ObjectMapper ATTRIBUTES_OBJECT_MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final Logger logger = LoggerFactory.getLogger(AttributeServiceImpl.class); private final List CUSTOM_ATTRIBUTE_COMPLIANT_RESOURCES = List.of( Resource.CERTIFICATE, @@ -303,11 +307,27 @@ public void updateAttributeContent(UUID objectUuid, List at @Override public void updateAttributeContent(UUID objectUuid, UUID attributeUuid, List attributeContent, Resource resource) throws NotFoundException { AttributeDefinition attributeDefinition = getAttributeDefinition(SecuredUUID.fromUUID(attributeUuid), AttributeType.CUSTOM); - CustomAttributeProperties props = attributeDefinition.getAttributeDefinition(CustomAttribute.class).getProperties(); - if (props.isReadOnly()) { - throw new ValidationException(ValidationError.create("Cannot update content for readonly attribute")); + CustomAttribute customAttribute = attributeDefinition.getAttributeDefinition(CustomAttribute.class); + + // validate setting of readonly attribute when it is not removal + if (customAttribute.getProperties().isReadOnly() && attributeContent != null) { + // content has to have same amount of items (in case of lists) + if (attributeContent.size() != customAttribute.getContent().size()) { + throw new ValidationException(ValidationError.create("Cannot change content of readonly attribute")); + } + + // convert to specific attribute content class based on content type and compare each item individually for equality + var clazz = AttributeContentType.getClass(customAttribute.getContentType()); + for (int i = 0; i < attributeContent.size(); i++) { + BaseAttributeContent attributeContentMapped = (BaseAttributeContent) ATTRIBUTES_OBJECT_MAPPER.convertValue(attributeContent.get(i), clazz); + BaseAttributeContent definitionContentMapped = (BaseAttributeContent) ATTRIBUTES_OBJECT_MAPPER.convertValue(customAttribute.getContent().get(i), clazz); + if (!attributeContentMapped.equals(definitionContentMapped)) { + throw new ValidationException(ValidationError.create("Cannot change content of readonly attribute")); + } + } } + // find existing content for this resource and attribute List attributeContent2Objects = attributeContent2ObjectRepository .findByObjectUuidAndObjectTypeAndAttributeContentAttributeDefinitionUuid( objectUuid, @@ -315,10 +335,13 @@ public void updateAttributeContent(UUID objectUuid, UUID attributeUuid, List getResourceSearchableFieldInformation(Resource resource) { final List searchFieldDataByGroupDtos = new ArrayList<>(); - final List metadataSearchFieldObject = attributeContentRepository.findDistinctAttributeContentNamesByAttrTypeAndObjType(resource,List.of(AttributeType.META)); + final List metadataSearchFieldObject = attributeContentRepository.findDistinctAttributeContentNamesByAttrTypeAndObjType(resource, List.of(AttributeType.META)); if (!metadataSearchFieldObject.isEmpty()) { searchFieldDataByGroupDtos.add(new SearchFieldDataByGroupDto(SearchHelper.prepareSearchForJSON(metadataSearchFieldObject), SearchGroup.META)); } - final List customAttrSearchFieldObject = attributeContentRepository.findDistinctAttributeContentNamesByAttrTypeAndObjType(resource,List.of(AttributeType.CUSTOM)); + final List customAttrSearchFieldObject = attributeContentRepository.findDistinctAttributeContentNamesByAttrTypeAndObjType(resource, List.of(AttributeType.CUSTOM)); if (!customAttrSearchFieldObject.isEmpty()) { searchFieldDataByGroupDtos.add(new SearchFieldDataByGroupDto(SearchHelper.prepareSearchForJSON(customAttrSearchFieldObject), SearchGroup.CUSTOM)); } @@ -523,7 +546,7 @@ public List getResourceObjectUuidsByFilters(Resource resource, SecurityFil return null; } - final List searchFieldObjects = attributeContentRepository.findDistinctAttributeContentNamesByAttrTypeAndObjType(resource,List.of(AttributeType.CUSTOM, AttributeType.META)); + final List searchFieldObjects = attributeContentRepository.findDistinctAttributeContentNamesByAttrTypeAndObjType(resource, List.of(AttributeType.CUSTOM, AttributeType.META)); final Sql2PredicateConverter.CriteriaQueryDataObject criteriaQueryDataObject = Sql2PredicateConverter.prepareQueryToSearchIntoAttributes(searchFieldObjects, attributesFilters, entityManager.getCriteriaBuilder(), resource); return attributeContent2ObjectRepository.findUsingSecurityFilterByCustomCriteriaQuery(securityFilter, criteriaQueryDataObject.getRoot(), criteriaQueryDataObject.getCriteriaQuery(), criteriaQueryDataObject.getPredicate()); } diff --git a/src/main/java/com/czertainly/core/service/impl/CallbackServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/CallbackServiceImpl.java index e6aced4b8..6531985ac 100644 --- a/src/main/java/com/czertainly/core/service/impl/CallbackServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/CallbackServiceImpl.java @@ -17,9 +17,7 @@ import com.czertainly.api.model.core.audit.ObjectType; import com.czertainly.api.model.core.audit.OperationType; import com.czertainly.api.model.core.auth.Resource; -import com.czertainly.api.model.core.connector.ConnectorDto; import com.czertainly.api.model.core.connector.FunctionGroupCode; -import com.czertainly.api.model.core.entity.EntityInstanceDto; import com.czertainly.core.aop.AuditLogged; import com.czertainly.core.dao.entity.AuthorityInstanceReference; import com.czertainly.core.dao.entity.Connector; @@ -32,12 +30,12 @@ import com.czertainly.core.util.AttributeDefinitionUtils; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.transaction.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import jakarta.transaction.Transactional; import java.util.List; import java.util.UUID; @@ -77,9 +75,8 @@ public class CallbackServiceImpl implements CallbackService { @AuditLogged(originator = ObjectType.FE, affected = ObjectType.ATTRIBUTES, operation = OperationType.CALLBACK) public Object callback(String uuid, FunctionGroupCode functionGroup, String kind, RequestAttributeCallback callback) throws ConnectorException, ValidationException { Connector connector = connectorService.getConnectorEntity(SecuredUUID.fromString(uuid)); - List definitions; - definitions = attributeApiClient.listAttributeDefinitions(connector.mapToDto(), functionGroup, kind); - AttributeCallback attributeCallback = getAttributeByName(callback.getName(), definitions); + List definitions = attributeApiClient.listAttributeDefinitions(connector.mapToDto(), functionGroup, kind); + AttributeCallback attributeCallback = getAttributeByName(callback.getName(), definitions, connector.getUuid()); AttributeDefinitionUtils.validateCallback(attributeCallback, callback); if (attributeCallback.getCallbackContext().equals("core/getCredentials")) { @@ -154,7 +151,7 @@ public Object resourceCallback(Resource resource, String resourceUuid, RequestAt ); } - AttributeCallback attributeCallback = getAttributeByName(callback.getName(), definitions); + AttributeCallback attributeCallback = getAttributeByName(callback.getName(), definitions, connector.getUuid()); AttributeDefinitionUtils.validateCallback(attributeCallback, callback); if (attributeCallback.getCallbackContext().equals("core/getCredentials")) { @@ -172,7 +169,7 @@ public Object resourceCallback(Resource resource, String resourceUuid, RequestAt } - private AttributeCallback getAttributeByName(String name, List attributes) throws NotFoundException { + private AttributeCallback getAttributeByName(String name, List attributes, UUID connectorUuid) throws NotFoundException { for (BaseAttribute attributeDefinition : attributes) { if (attributeDefinition.getName().equals(name)) { switch (attributeDefinition.getType()) { @@ -183,6 +180,13 @@ private AttributeCallback getAttributeByName(String name, List at } } } + + // if not present in definitions from connector, search in reference attributes in DB + DataAttribute referencedAttribute = attributeService.getReferenceAttribute(connectorUuid, name); + if (referencedAttribute != null) { + return referencedAttribute.getAttributeCallback(); + } + throw new NotFoundException(BaseAttribute.class, name); } diff --git a/src/main/java/com/czertainly/core/service/impl/CertificateServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/CertificateServiceImpl.java index 557c8fea4..0c99190aa 100644 --- a/src/main/java/com/czertainly/core/service/impl/CertificateServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/CertificateServiceImpl.java @@ -308,14 +308,25 @@ public void deleteCertificate(SecuredUUID uuid) throws NotFoundException { public void updateCertificateObjects(SecuredUUID uuid, CertificateUpdateObjectsDto request) throws NotFoundException, CertificateOperationException { logger.info("Updating certificate objects: RA {} group {} owner {}", request.getRaProfileUuid(), request.getGroupUuid(), request.getOwnerUuid()); if (request.getRaProfileUuid() != null) { - switchRaProfile(uuid, SecuredUUID.fromString(request.getRaProfileUuid())); + switchRaProfile(uuid, request.getRaProfileUuid().isEmpty() ? null : SecuredUUID.fromString(request.getRaProfileUuid())); } if (request.getGroupUuid() != null) { - updateCertificateGroup(uuid, SecuredUUID.fromString(request.getGroupUuid())); + updateCertificateGroup(uuid, request.getGroupUuid().isEmpty() ? null : SecuredUUID.fromString(request.getGroupUuid())); } if (request.getOwnerUuid() != null) { - updateOwner(uuid, request.getOwnerUuid()); + updateOwner(uuid, request.getOwnerUuid().isEmpty() ? null : request.getOwnerUuid(), null); } + if (request.getTrustedCa() != null) { + updateTrustedCaMark(uuid, request.getTrustedCa()); + } + } + + private void updateTrustedCaMark(SecuredUUID uuid, Boolean trustedCa) throws NotFoundException { + Certificate certificate = getCertificateEntity(uuid); + if (certificate.getTrustedCa() == null) { + throw new ValidationException("Trying to mark certificate as trusted CA when certificate is not CA."); + } + certificate.setTrustedCa(trustedCa); } @Async @@ -410,7 +421,8 @@ public List getSearchableFieldInformationByGroup() { SearchHelper.prepareSearch(SearchFieldNameEnum.PUBLIC_KEY_ALGORITHM, new ArrayList<>(certificateRepository.findDistinctPublicKeyAlgorithm())), SearchHelper.prepareSearch(SearchFieldNameEnum.KEY_SIZE, new ArrayList<>(certificateRepository.findDistinctKeySize())), SearchHelper.prepareSearch(SearchFieldNameEnum.KEY_USAGE, serializedListOfStringToListOfObject(certificateRepository.findDistinctKeyUsage())), - SearchHelper.prepareSearch(SearchFieldNameEnum.PRIVATE_KEY) + SearchHelper.prepareSearch(SearchFieldNameEnum.PRIVATE_KEY), + SearchHelper.prepareSearch(SearchFieldNameEnum.TRUSTED_CA) ); fields = new ArrayList<>(fields); @@ -564,52 +576,92 @@ private Certificate constructCertificateChain(Certificate certificate, List certificateContent = getCertificateContent(List.of(uuid.toString())); + if (certificateContent.size() == 0) { + throw new ValidationException("Cannot download certificate chain, the end certificate is not issued."); + } CertificateChainResponseDto certificateChainResponseDto = getCertificateChain(uuid, withEndCertificate); List certificateChain = certificateChainResponseDto.getCertificates(); CertificateChainDownloadResponseDto certificateChainDownloadResponseDto = new CertificateChainDownloadResponseDto(); certificateChainDownloadResponseDto.setCompleteChain(certificateChainResponseDto.isCompleteChain()); certificateChainDownloadResponseDto.setFormat(certificateFormat); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(new OutputStreamWriter(byteArrayOutputStream)); - List x509CertificateChain = new ArrayList<>(); - for (CertificateDto certificateDto : certificateChain) { - Certificate certificateInstance; - certificateInstance = getCertificateEntity(SecuredUUID.fromString(certificateDto.getUuid())); - X509Certificate x509Certificate; - try { - x509Certificate = CertificateUtil.getX509Certificate(certificateInstance.getCertificateContent().getContent()); - } catch (CertificateException e) { - certificateChainDownloadResponseDto.setCompleteChain(false); - break; + certificateChainDownloadResponseDto.setEncoding(encoding); + certificateChainDownloadResponseDto.setContent(getDownloadedContent(certificateChain, certificateFormat, encoding, true)); + return certificateChainDownloadResponseDto; + } + + @Override + @ExternalAuthorization(resource = Resource.CERTIFICATE, action = ResourceAction.DETAIL) + public CertificateDownloadResponseDto downloadCertificate(String uuid, CertificateFormat certificateFormat, CertificateFormatEncoding encoding) throws CertificateException, NotFoundException, IOException { + CertificateDetailDto certificate = getCertificate(SecuredUUID.fromString(uuid)); + if (certificate.getCertificateContent() == null) { + throw new ValidationException("Cannot download the certificate, certificate is not issued."); + } + CertificateDownloadResponseDto certificateDownloadResponseDto = new CertificateDownloadResponseDto(); + certificateDownloadResponseDto.setFormat(certificateFormat); + certificateDownloadResponseDto.setEncoding(encoding); + certificateDownloadResponseDto.setContent(getDownloadedContent(List.of(certificate), certificateFormat, encoding, false)); + return certificateDownloadResponseDto; + } + + private String getDownloadedContent(List certificateDetailDtos, CertificateFormat certificateFormat, CertificateFormatEncoding encoding, boolean downloadingChain) throws NotFoundException, CertificateException { + if (certificateFormat == CertificateFormat.RAW) { + if (encoding == CertificateFormatEncoding.DER) { + if (downloadingChain) { + throw new ValidationException("DER encoding of raw format is unsupported for certificate chain."); + } + return getCertificateEntity(SecuredUUID.fromString(certificateDetailDtos.get(0).getUuid())).getCertificateContent().getContent(); } - if (certificateFormat == CertificateFormat.PEM) { + // Encoding is PEM otherwise + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(new OutputStreamWriter(byteArrayOutputStream)); + for (CertificateDto certificateDto : certificateDetailDtos) { + Certificate certificateInstance = getCertificateEntity(SecuredUUID.fromString(certificateDto.getUuid())); + String content = certificateInstance.getCertificateContent().getContent(); + X509Certificate x509Certificate; + x509Certificate = CertificateUtil.getX509Certificate(content); try { jcaPEMWriter.writeObject(x509Certificate); jcaPEMWriter.flush(); } catch (IOException e) { - throw new CertificateException("Could not write certificate chain as PEM format: " + e.getMessage()); + throw new CertificateException("Could not write downloaded content as PEM format: " + e.getMessage()); } - } else { + } + return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); + } + // Formatting is PKCS7 otherwise + else { + List x509CertificateChain = new ArrayList<>(); + for (CertificateDto certificateDto : certificateDetailDtos) { + Certificate certificateInstance = getCertificateEntity(SecuredUUID.fromString(certificateDto.getUuid())); + X509Certificate x509Certificate; + x509Certificate = CertificateUtil.getX509Certificate(certificateInstance.getCertificateContent().getContent()); x509CertificateChain.add(x509Certificate); } - } - if (certificateFormat == CertificateFormat.PKCS7) { try { CMSSignedDataGenerator generator = new CMSSignedDataGenerator(); generator.addCertificates(new JcaCertStore(x509CertificateChain)); byte[] encoded = generator.generate(new CMSProcessableByteArray(new byte[0])).getEncoded(); - ContentInfo contentInfo = ContentInfo.getInstance(ASN1Primitive.fromByteArray(encoded)); - jcaPEMWriter.writeObject(contentInfo); - jcaPEMWriter.flush(); + if (encoding == CertificateFormatEncoding.PEM) { + ContentInfo contentInfo = ContentInfo.getInstance(ASN1Primitive.fromByteArray(encoded)); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(new OutputStreamWriter(byteArrayOutputStream)); + jcaPEMWriter.writeObject(contentInfo); + jcaPEMWriter.flush(); + return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); + } + // Encoding is DER otherwise + else { + return Base64.getEncoder().encodeToString(encoded); + } } catch (Exception e) { - throw new CertificateException("Could not write certificate chain as PKCS#7 format: " + e.getMessage()); + throw new CertificateException("Could not write downloaded content as PKCS#7 format: " + e.getMessage()); } } - certificateChainDownloadResponseDto.setContent(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())); - return certificateChainDownloadResponseDto; } + @Override public void validate(Certificate certificate) { CertificateChainResponseDto certificateChainResponse = getCertificateChainInternal(certificate, true); @@ -794,6 +846,7 @@ private CertificateContent checkAddCertificateContent(String fingerprint, String @Override @AuditLogged(originator = ObjectType.FE, affected = ObjectType.CERTIFICATE, operation = OperationType.CREATE) + @ExternalAuthorization(resource = Resource.CERTIFICATE, action = ResourceAction.CREATE) public CertificateDetailDto upload(UploadCertificateRequestDto request) throws CertificateException, NoSuchAlgorithmException, AlreadyExistException { X509Certificate certificate = CertificateUtil.parseUploadedCertificateContent(request.getCertificate()); String fingerprint = CertificateUtil.getThumbprint(certificate); @@ -920,11 +973,10 @@ public int updateCertificatesStatusScheduled() { int certificatesUpdated = 0; logger.info(MarkerFactory.getMarker("scheduleInfo"), "Scheduled certificate status update. Batch size {}/{} certificates", certificates.size(), totalCertificates); for (final Certificate certificate : certificates) { - CertificateValidationStatus oldStatus = certificate.getValidationStatus(); TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { validate(certificate); - if (certificate.getRaProfileUuid() != null && certificate.getComplianceStatus() == null) { + if (certificate.getRaProfileUuid() != null && certificate.getComplianceStatus() == ComplianceStatus.NOT_CHECKED) { complianceService.checkComplianceOfCertificate(certificate); } @@ -935,15 +987,6 @@ public int updateCertificatesStatusScheduled() { continue; } - if (!oldStatus.equals(certificate.getValidationStatus())) { - eventProducer.produceCertificateStatusChangeEventMessage(certificate.getUuid(), CertificateEvent.UPDATE_VALIDATION_STATUS, CertificateEventStatus.SUCCESS, oldStatus, certificate.getValidationStatus()); - try { - notificationProducer.produceNotificationCertificateStatusChanged(oldStatus, certificate.getValidationStatus(), certificate.mapToListDto()); - } catch (Exception e) { - logger.error("Sending certificate {} notification for change of status {} failed. Error: {}", certificate.getUuid(), certificate.getValidationStatus().getCode(), e.getMessage(), e); - } - } - ++certificatesUpdated; } logger.info(MarkerFactory.getMarker("scheduleInfo"), "Certificates status updated for {}/{} certificates", certificatesUpdated, certificates.size()); @@ -1421,162 +1464,194 @@ private String downloadChain(String chainUrl) { private void switchRaProfile(SecuredUUID uuid, SecuredUUID raProfileUuid) throws NotFoundException, CertificateOperationException { Certificate certificate = getCertificateEntity(uuid); - RaProfile newRaProfile = raProfileRepository.findByUuid(raProfileUuid) - .orElseThrow(() -> new NotFoundException(RaProfile.class, raProfileUuid)); + // check if there is change in RA profile compared to current state + if ((raProfileUuid == null && certificate.getRaProfileUuid() == null) + || (raProfileUuid != null && certificate.getRaProfileUuid() != null) && certificate.getRaProfileUuid().toString().equals(raProfileUuid.toString())) { + return; + } + + // removing RA profile + RaProfile newRaProfile = null; RaProfile currentRaProfile = certificate.getRaProfile(); - String currentRaProfileName = UNDEFINED_CERTIFICATE_OBJECT_NAME; - if (currentRaProfile != null) { - if (currentRaProfile.getUuid().toString().equals(raProfileUuid.toString())) { - return; + String newRaProfileName = UNDEFINED_CERTIFICATE_OBJECT_NAME; + String currentRaProfileName = currentRaProfile != null ? currentRaProfile.getName() : UNDEFINED_CERTIFICATE_OBJECT_NAME; + CertificateIdentificationResponseDto response = null; + if (raProfileUuid != null) { + newRaProfile = raProfileRepository.findByUuid(raProfileUuid).orElseThrow(() -> new NotFoundException(RaProfile.class, raProfileUuid)); + newRaProfileName = newRaProfile.getName(); + + // identify certificate by new authority + CertificateIdentificationRequestDto requestDto = new CertificateIdentificationRequestDto(); + requestDto.setCertificate(certificate.getCertificateContent().getContent()); + requestDto.setRaProfileAttributes(AttributeDefinitionUtils.getClientAttributes(newRaProfile.mapToDto().getAttributes())); + try { + response = certificateApiClient.identifyCertificate( + newRaProfile.getAuthorityInstanceReference().getConnector().mapToDto(), + newRaProfile.getAuthorityInstanceReference().getAuthorityInstanceUuid(), + requestDto); + } catch (ConnectorException e) { + certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.FAILED, String.format("Certificate not identified by authority of new RA profile %s. Certificate needs to be reissued.", newRaProfile.getName()), ""); + throw new CertificateOperationException(String.format("Cannot switch RA profile for certificate. Certificate not identified by authority of new RA profile %s. Certificate: %s", newRaProfile.getName(), certificate)); + } catch (ValidationException e) { + certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.FAILED, String.format("Certificate identified by authority of new RA profile %s but not valid according to RA profile attributes. Certificate needs to be reissued.", newRaProfile.getName()), ""); + throw new CertificateOperationException(String.format("Cannot switch RA profile for certificate. Certificate identified by authority of new RA profile %s but not valid according to RA profile attributes. Certificate: %s", newRaProfile.getName(), certificate)); } - currentRaProfileName = certificate.getRaProfile().getName(); } - // identify certificate by new authority - CertificateIdentificationResponseDto response; - CertificateIdentificationRequestDto requestDto = new CertificateIdentificationRequestDto(); - requestDto.setCertificate(certificate.getCertificateContent().getContent()); - requestDto.setRaProfileAttributes(AttributeDefinitionUtils.getClientAttributes(newRaProfile.mapToDto().getAttributes())); - try { - response = certificateApiClient.identifyCertificate( - newRaProfile.getAuthorityInstanceReference().getConnector().mapToDto(), - newRaProfile.getAuthorityInstanceReference().getAuthorityInstanceUuid(), - requestDto); - } catch (ConnectorException e) { - certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.FAILED, String.format("Certificate not identified by authority of new RA profile %s. Certificate needs to be reissued.", newRaProfile.getName()), ""); - throw new CertificateOperationException(String.format("Cannot switch RA profile for certificate. Certificate not identified by authority of new RA profile %s. Certificate: %s", newRaProfile.getName(), certificate)); - } catch (ValidationException e) { - certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.FAILED, String.format("Certificate identified by authority of new RA profile %s but not valid according to RA profile attributes. Certificate needs to be reissued.", newRaProfile.getName()), ""); - throw new CertificateOperationException(String.format("Cannot switch RA profile for certificate. Certificate identified by authority of new RA profile %s but not valid according to RA profile attributes. Certificate: %s", newRaProfile.getName(), certificate)); - } + certificate.setRaProfile(newRaProfile); + certificateRepository.save(certificate); // delete old metadata if (currentRaProfile != null) { metadataService.deleteConnectorMetadata(currentRaProfile.getAuthorityInstanceReference().getConnectorUuid(), certificate.getUuid(), Resource.CERTIFICATE, null, null); } - // save metadata for identified certificate - UUID connectorUuid = newRaProfile.getAuthorityInstanceReference().getConnectorUuid(); - metadataService.createMetadataDefinitions(connectorUuid, response.getMeta()); - metadataService.createMetadata(connectorUuid, certificate.getUuid(), null, null, response.getMeta(), Resource.CERTIFICATE, null); + // save metadata for identified certificate and run compliance + if (newRaProfile != null) { + UUID connectorUuid = newRaProfile.getAuthorityInstanceReference().getConnectorUuid(); + metadataService.createMetadataDefinitions(connectorUuid, response.getMeta()); + metadataService.createMetadata(connectorUuid, certificate.getUuid(), null, null, response.getMeta(), Resource.CERTIFICATE, null); - certificate.setRaProfile(newRaProfile); - certificateRepository.save(certificate); - try { - complianceService.checkComplianceOfCertificate(certificate); - } catch (ConnectorException e) { - logger.error("Error when checking compliance:", e); + try { + complianceService.checkComplianceOfCertificate(certificate); + } catch (ConnectorException e) { + logger.error("Error when checking compliance:", e); + } } - certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.SUCCESS, currentRaProfileName + " -> " + newRaProfile.getName(), ""); + + certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.SUCCESS, currentRaProfileName + " -> " + newRaProfileName, ""); } private void updateCertificateGroup(SecuredUUID uuid, SecuredUUID groupUuid) throws NotFoundException { Certificate certificate = getCertificateEntity(uuid); - Group group = groupRepository.findByUuid(groupUuid) - .orElseThrow(() -> new NotFoundException(Group.class, groupUuid)); - String originalGroup = UNDEFINED_CERTIFICATE_OBJECT_NAME; - if (certificate.getGroup() != null) { - originalGroup = certificate.getGroup().getName(); + // check if there is change in group compared to current state + if ((groupUuid == null && certificate.getGroupUuid() == null) + || (groupUuid != null && certificate.getGroupUuid() != null) && certificate.getGroupUuid().toString().equals(groupUuid.toString())) { + return; + } + + Group newGroup = null; + Group currentGroup = certificate.getGroup(); + String newGroupName = UNDEFINED_CERTIFICATE_OBJECT_NAME; + String currentGroupName = currentGroup != null ? currentGroup.getName() : UNDEFINED_CERTIFICATE_OBJECT_NAME; + if (groupUuid != null) { + newGroup = groupRepository.findByUuid(groupUuid).orElseThrow(() -> new NotFoundException(Group.class, groupUuid)); + newGroupName = newGroup.getName(); } - certificate.setGroup(group); + + certificate.setGroup(newGroup); certificateRepository.save(certificate); - certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_GROUP, CertificateEventStatus.SUCCESS, originalGroup + " -> " + group.getName(), ""); + certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_GROUP, CertificateEventStatus.SUCCESS, currentGroupName + " -> " + newGroupName, ""); } - private void updateOwner(SecuredUUID uuid, String ownerUuid) throws NotFoundException { + private void updateOwner(SecuredUUID uuid, String ownerUuid, String ownerName) throws NotFoundException { Certificate certificate = getCertificateEntity(uuid); // if there is no change, do not update and save request to Auth service - if (certificate.getOwnerUuid() != null && certificate.getOwnerUuid().toString().equals(ownerUuid)) return; + if ((ownerUuid == null && certificate.getOwnerUuid() == null) + || (ownerUuid != null && certificate.getOwnerUuid() != null) && certificate.getOwnerUuid().toString().equals(ownerUuid)) { + return; + } - String originalOwner = certificate.getOwner(); - if (originalOwner == null || originalOwner.isEmpty()) { - originalOwner = UNDEFINED_CERTIFICATE_OBJECT_NAME; + UUID newOwnerUuid = null; + String newOwnerName = ownerName; + String currentOwnerName = certificate.getOwner(); + if (currentOwnerName == null || currentOwnerName.isEmpty()) { + currentOwnerName = UNDEFINED_CERTIFICATE_OBJECT_NAME; + } + if (ownerUuid != null) { + newOwnerUuid = UUID.fromString(ownerUuid); + if (ownerName == null) { + UserDetailDto userDetail = userManagementApiClient.getUserDetail(ownerUuid); + newOwnerName = userDetail.getUsername(); + } } - UserDetailDto userDetail = userManagementApiClient.getUserDetail(ownerUuid); - certificate.setOwner(userDetail.getUsername()); - certificate.setOwnerUuid(UUID.fromString(userDetail.getUuid())); + + certificate.setOwner(newOwnerName); + certificate.setOwnerUuid(newOwnerUuid); certificateRepository.save(certificate); - certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_OWNER, CertificateEventStatus.SUCCESS, originalOwner + " -> " + userDetail.getUsername(), ""); + certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.UPDATE_OWNER, CertificateEventStatus.SUCCESS, String.format("%s -> %s", currentOwnerName, newOwnerName == null ? UNDEFINED_CERTIFICATE_OBJECT_NAME : newOwnerName), ""); } private void bulkUpdateRaProfile(SecurityFilter filter, MultipleCertificateObjectUpdateDto request) throws NotFoundException { - List batchHistoryOperationList = new ArrayList<>(); - RaProfile raProfile = raProfileRepository.findByUuid(SecuredUUID.fromString(request.getRaProfileUuid())) - .orElseThrow(() -> new NotFoundException(RaProfile.class, request.getRaProfileUuid())); + boolean removeRaProfile = request.getRaProfileUuid().isEmpty(); if (request.getFilters() == null || request.getFilters().isEmpty() || (request.getCertificateUuids() != null && !request.getCertificateUuids().isEmpty())) { - for (String certificateUuid : request.getCertificateUuids()) { + for (String certificateUuidString : request.getCertificateUuids()) { try { - permissionEvaluator.certificate(SecuredUUID.fromString(certificateUuid)); - switchRaProfile(SecuredUUID.fromString(certificateUuid), SecuredUUID.fromString(request.getRaProfileUuid())); + SecuredUUID certificateUuid = SecuredUUID.fromString(certificateUuidString); + permissionEvaluator.certificate(certificateUuid); + switchRaProfile(certificateUuid, removeRaProfile ? null : SecuredUUID.fromString(request.getRaProfileUuid())); } catch (CertificateOperationException e) { logger.warn(e.getMessage()); } } } else { + RaProfile raProfile = removeRaProfile ? null : raProfileRepository.findByUuid(SecuredUUID.fromString(request.getRaProfileUuid())) + .orElseThrow(() -> new NotFoundException(RaProfile.class, request.getRaProfileUuid())); + String data = searchService.createCriteriaBuilderString(filter, false); - if (!data.equals("")) { + if (!data.isEmpty()) { data = "WHERE " + data; } - String profileUpdateQuery = "UPDATE Certificate c SET c.raProfile = " + raProfile.getUuid() + searchService.getCompleteSearchQuery(request.getFilters(), "certificate", data, getSearchableFieldInformation(), true, false).replace("GROUP BY c.id ORDER BY c.id DESC", ""); + + String profileUpdateQuery = "UPDATE Certificate c SET c.raProfile = " + (removeRaProfile ? "NULL" : raProfile.getUuid()) + searchService.getCompleteSearchQuery(request.getFilters(), "certificate", data, getSearchableFieldInformation(), true, false).replace("GROUP BY c.id ORDER BY c.id DESC", ""); certificateRepository.bulkUpdateQuery(profileUpdateQuery); - certificateEventHistoryService.addEventHistoryForRequest(request.getFilters(), "Certificate", getSearchableFieldInformation(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.SUCCESS, "RA Profile Name: " + raProfile.getName()); + certificateEventHistoryService.addEventHistoryForRequest(request.getFilters(), "Certificate", getSearchableFieldInformation(), CertificateEvent.UPDATE_RA_PROFILE, CertificateEventStatus.SUCCESS, "RA Profile Name: " + (removeRaProfile ? UNDEFINED_CERTIFICATE_OBJECT_NAME : raProfile.getName())); bulkUpdateRaProfileComplianceCheck(request.getFilters()); } } private void bulkUpdateCertificateGroup(SecurityFilter filter, MultipleCertificateObjectUpdateDto request) throws NotFoundException { - Group group = groupRepository.findByUuid(SecuredUUID.fromString(request.getGroupUuid())) - .orElseThrow(() -> new NotFoundException(Group.class, request.getGroupUuid())); - List batchHistoryOperationList = new ArrayList<>(); + boolean removeGroup = request.getGroupUuid().isEmpty(); if (request.getFilters() == null || request.getFilters().isEmpty() || (request.getCertificateUuids() != null && !request.getCertificateUuids().isEmpty())) { - List batchOperationList = new ArrayList<>(); - - for (String certificateUuid : request.getCertificateUuids()) { - Certificate certificate = getCertificateEntity(SecuredUUID.fromString(certificateUuid)); - permissionEvaluator.certificate(certificate.getSecuredUuid()); - batchHistoryOperationList.add(certificateEventHistoryService.getEventHistory(CertificateEvent.UPDATE_GROUP, CertificateEventStatus.SUCCESS, certificate.getGroup() != null ? certificate.getGroup().getName() : UNDEFINED_CERTIFICATE_OBJECT_NAME + " -> " + group.getName(), "", certificate)); - certificate.setGroup(group); - batchOperationList.add(certificate); + for (String certificateUuidString : request.getCertificateUuids()) { + SecuredUUID certificateUuid = SecuredUUID.fromString(certificateUuidString); + permissionEvaluator.certificate(certificateUuid); + updateCertificateGroup(certificateUuid, removeGroup ? null : SecuredUUID.fromString(request.getGroupUuid())); } - certificateRepository.saveAll(batchOperationList); - certificateEventHistoryService.asyncSaveAllInBatch(batchHistoryOperationList); } else { + Group group = groupRepository.findByUuid(SecuredUUID.fromString(request.getGroupUuid())) + .orElseThrow(() -> new NotFoundException(Group.class, request.getGroupUuid())); + String data = searchService.createCriteriaBuilderString(filter, false); - if (!data.equals("")) { + if (!data.isEmpty()) { data = "WHERE " + data; } - String groupUpdateQuery = "UPDATE Certificate c SET c.group = " + group.getUuid() + searchService.getCompleteSearchQuery(request.getFilters(), "certificate", data, getSearchableFieldInformation(), true, false).replace("GROUP BY c.id ORDER BY c.id DESC", ""); + String groupUpdateQuery = "UPDATE Certificate c SET c.group = " + (removeGroup ? "NULL" : group.getUuid()) + searchService.getCompleteSearchQuery(request.getFilters(), "certificate", data, getSearchableFieldInformation(), true, false).replace("GROUP BY c.id ORDER BY c.id DESC", ""); certificateRepository.bulkUpdateQuery(groupUpdateQuery); - certificateEventHistoryService.addEventHistoryForRequest(request.getFilters(), "Certificate", getSearchableFieldInformation(), CertificateEvent.UPDATE_GROUP, CertificateEventStatus.SUCCESS, "Group Name: " + group.getName()); + certificateEventHistoryService.addEventHistoryForRequest(request.getFilters(), "Certificate", getSearchableFieldInformation(), CertificateEvent.UPDATE_GROUP, CertificateEventStatus.SUCCESS, "Group Name: " + (removeGroup ? UNDEFINED_CERTIFICATE_OBJECT_NAME : group.getName())); } } private void bulkUpdateOwner(SecurityFilter filter, MultipleCertificateObjectUpdateDto request) throws NotFoundException { - UserDetailDto userDetail = userManagementApiClient.getUserDetail(request.getOwnerUuid()); + boolean removeOwner = request.getOwnerUuid().isEmpty(); + String ownerUuid = null; + String ownerName = null; + if (!removeOwner) { + UserDetailDto userDetail = userManagementApiClient.getUserDetail(request.getOwnerUuid()); + ownerUuid = request.getOwnerUuid(); + ownerName = userDetail.getUsername(); + } List batchHistoryOperationList = new ArrayList<>(); if (request.getFilters() == null || request.getFilters().isEmpty() || (request.getCertificateUuids() != null && !request.getCertificateUuids().isEmpty())) { List batchOperationList = new ArrayList<>(); - for (String certificateUuid : request.getCertificateUuids()) { - Certificate certificate = getCertificateEntity(SecuredUUID.fromString(certificateUuid)); - permissionEvaluator.certificate(certificate.getSecuredUuid()); - batchHistoryOperationList.add(certificateEventHistoryService.getEventHistory(CertificateEvent.UPDATE_OWNER, CertificateEventStatus.SUCCESS, certificate.getOwner() + " -> " + request.getOwnerUuid(), "", certificate)); - certificate.setOwner(userDetail.getUsername()); - certificate.setOwnerUuid(UUID.fromString(userDetail.getUuid())); - batchOperationList.add(certificate); + for (String certificateUuidString : request.getCertificateUuids()) { + SecuredUUID certificateUuid = SecuredUUID.fromString(certificateUuidString); + permissionEvaluator.certificate(certificateUuid); + updateOwner(certificateUuid, ownerUuid, ownerName); } certificateRepository.saveAll(batchOperationList); certificateEventHistoryService.asyncSaveAllInBatch(batchHistoryOperationList); } else { String data = searchService.createCriteriaBuilderString(filter, false); - if (!data.equals("")) { + if (!data.isEmpty()) { data = "WHERE " + data; } - String ownerUpdateQuery = "UPDATE Certificate c SET c.owner = '" + userDetail.getUsername() + "',c.owner_uuid = '" + UUID.fromString(userDetail.getUuid()) + "' " + searchService.getCompleteSearchQuery(request.getFilters(), "certificate", data, getSearchableFieldInformation(), true, false).replace("GROUP BY c.id ORDER BY c.id DESC", ""); + String ownerUpdateQuery = "UPDATE Certificate c SET c.owner = '" + (removeOwner ? "NULL" : ownerName) + "',c.owner_uuid = '" + (removeOwner ? "NULL" : UUID.fromString(request.getOwnerUuid())) + "' " + searchService.getCompleteSearchQuery(request.getFilters(), "certificate", data, getSearchableFieldInformation(), true, false).replace("GROUP BY c.id ORDER BY c.id DESC", ""); certificateRepository.bulkUpdateQuery(ownerUpdateQuery); - certificateEventHistoryService.addEventHistoryForRequest(request.getFilters(), "Certificate", getSearchableFieldInformation(), CertificateEvent.UPDATE_OWNER, CertificateEventStatus.SUCCESS, "Owner: " + userDetail.getUsername()); + certificateEventHistoryService.addEventHistoryForRequest(request.getFilters(), "Certificate", getSearchableFieldInformation(), CertificateEvent.UPDATE_OWNER, CertificateEventStatus.SUCCESS, "Owner: " + (removeOwner ? UNDEFINED_CERTIFICATE_OBJECT_NAME : ownerName)); } } diff --git a/src/main/java/com/czertainly/core/service/impl/ClientOperationServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/ClientOperationServiceImpl.java index 78205552a..8b789c2f7 100644 --- a/src/main/java/com/czertainly/core/service/impl/ClientOperationServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/ClientOperationServiceImpl.java @@ -177,6 +177,7 @@ public void editEndEntity(String raProfileName, String username, ClientEditEndEn caRequest.setSubjectDN(request.getSubjectDN()); caRequest.setSubjectAltName(request.getSubjectAltName()); caRequest.setExtensionData(request.getExtensionData()); + caRequest.setStatus(request.getStatus()); caRequest.setRaProfile(raProfile.mapToDto()); endEntityApiClient.updateEndEntity( diff --git a/src/main/java/com/czertainly/core/service/impl/CrlServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/CrlServiceImpl.java new file mode 100644 index 000000000..5c57c0cdf --- /dev/null +++ b/src/main/java/com/czertainly/core/service/impl/CrlServiceImpl.java @@ -0,0 +1,226 @@ +package com.czertainly.core.service.impl; + +import com.czertainly.api.exception.ValidationException; +import com.czertainly.api.model.core.authority.CertificateRevocationReason; +import com.czertainly.core.dao.entity.Certificate; +import com.czertainly.core.dao.entity.Crl; +import com.czertainly.core.dao.entity.CrlEntry; +import com.czertainly.core.dao.entity.CrlEntryId; +import com.czertainly.core.dao.repository.CertificateRepository; +import com.czertainly.core.dao.repository.CrlEntryRepository; +import com.czertainly.core.dao.repository.CrlRepository; +import com.czertainly.core.service.CrlService; +import com.czertainly.core.util.CrlUtil; +import com.czertainly.core.util.CzertainlyX500NameStyle; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.security.cert.*; +import java.util.*; + +@Service +public class CrlServiceImpl implements CrlService { + + private CertificateRepository certificateRepository; + + private CrlRepository crlRepository; + + private CrlEntryRepository crlEntryRepository; + + @Autowired + public void setCertificateRepository(CertificateRepository certificateRepository) { + this.certificateRepository = certificateRepository; + } + + @Autowired + public void setCrlRepository(CrlRepository crlRepository) { + this.crlRepository = crlRepository; + } + + @Autowired + public void setCrlEntryRepository(CrlEntryRepository crlEntryRepository) { + this.crlEntryRepository = crlEntryRepository; + } + + @Override + public Crl createCrlAndCrlEntries(byte[] crlDistributionPointsEncoded, String issuerDn, String issuerSerialNumber, UUID caCertificateUuid, String oldCrlNumber) throws IOException { + List crlUrls = CrlUtil.getCDPFromCertificate(crlDistributionPointsEncoded); + + Crl crl = null; + + for (String crlUrl : crlUrls) { + X509CRL X509Crl; + try { + X509Crl = CrlUtil.getX509Crl(crlUrl); + } catch (Exception e) { + // Failed to read content from URL, continue to next URL + continue; + } + + String crlNumber = JcaX509ExtensionUtils.parseExtensionValue(X509Crl.getExtensionValue(Extension.cRLNumber.getId())).toString(); + if (Objects.equals(crlNumber, oldCrlNumber)) return null; + + crl = new Crl(); + List crlEntries = new ArrayList<>(); + crl.setNextUpdate(X509Crl.getNextUpdate()); + byte[] issuerDnPrincipalEncoded = X509Crl.getIssuerX500Principal().getEncoded(); + crl.setCrlIssuerDn(X500Name.getInstance(CzertainlyX500NameStyle.NORMALIZED, issuerDnPrincipalEncoded).toString()); + crl.setSerialNumber(issuerSerialNumber); + crl.setIssuerDn(issuerDn); + crl.setCaCertificateUuid(caCertificateUuid); + crl.setCrlNumber(crlNumber); + crl.setCrlEntries(crlEntries); + crlRepository.save(crl); + + Set crlCertificates = X509Crl.getRevokedCertificates(); + if (crlCertificates != null) { + Date lastRevocationDate = new Date(0); + for (X509CRLEntry x509CRLEntry : crlCertificates) { + CrlEntry crlEntry = createCrlEntry(x509CRLEntry, crl); + crlEntries.add(crlEntry); + if (crlEntry.getRevocationDate().after(lastRevocationDate)) + lastRevocationDate = crlEntry.getRevocationDate(); + } + crl.setLastRevocationDate(lastRevocationDate); + crlRepository.save(crl); + } + + // Managed to process a CRL url and do not need to try other URLs + break; + } + return crl; + } + + @Override + public Crl updateCrlAndCrlEntriesFromDeltaCrl(X509Certificate certificate, Crl crl, String issuerDn, String issuerSerialNumber, UUID caCertificateUuid) throws IOException { + List deltaCrlUrls = CrlUtil.getCDPFromCertificate(certificate.getExtensionValue(Extension.freshestCRL.getId())); + for (String deltaCrlUrl : deltaCrlUrls) { + X509CRL deltaCrl; + try { + deltaCrl = CrlUtil.getX509Crl(deltaCrlUrl); + } catch (Exception e) { + // Failed to read content from URL, continue to next URL + continue; + } + String deltaCrlIssuer = X500Name.getInstance(CzertainlyX500NameStyle.NORMALIZED, deltaCrl.getIssuerX500Principal().getEncoded()).toString(); + // Compare CRL issuer with issuer stored in CRL entity, delta CRL is invalid if they are not the same + if (!Objects.equals(deltaCrlIssuer, crl.getCrlIssuerDn())) + throw new ValidationException("Delta CRL issuer not same as issuer stored in CRL entity"); + + // Compare DeltaCRLIndicator with base CRL number, if they are not equal, try to get newer CRL + String deltaCrlIndicator = JcaX509ExtensionUtils.parseExtensionValue(deltaCrl.getExtensionValue(Extension.deltaCRLIndicator.getId())).toString(); + if (!Objects.equals(deltaCrlIndicator, crl.getCrlNumber())) { + Crl newCrl = createCrlAndCrlEntries(certificate.getExtensionValue(Extension.cRLDistributionPoints.getId()), issuerDn, issuerSerialNumber, caCertificateUuid, crl.getCrlNumber()); + // If received CRL is null, it means it is the old one again, and we are not able to set delta CRL properly + if (newCrl == null) + throw new ValidationException("Unable to get CRL with base CRL number equal to DeltaCRLIndicator"); + // Otherwise delete the old CRL and continue with the new CRL + crlRepository.delete(crl); + crl = newCrl; + } + updateDeltaCrl(crl, deltaCrl); + // Managed to process a delta CRL url and do not need to try other URLs + break; + } + return crl; + } + + + @Override + public Crl getCurrentCrl(X509Certificate certificate, X509Certificate issuerCertificate) throws IOException { + byte[] issuerDnPrincipalEncoded = certificate.getIssuerX500Principal().getEncoded(); + String issuerDn = X500Name.getInstance(CzertainlyX500NameStyle.NORMALIZED, issuerDnPrincipalEncoded).toString(); + String issuerSerialNumber = issuerCertificate.getSerialNumber().toString(16); + Optional crlOptional = crlRepository.findByIssuerDnAndSerialNumber(issuerDn, issuerSerialNumber); + Optional caCertificate = certificateRepository.findBySubjectDnNormalizedAndSerialNumber(issuerDn, issuerSerialNumber); + byte[] crlDistributionPoints = certificate.getExtensionValue(Extension.cRLDistributionPoints.getId()); + + Crl crl = null; + UUID caCertificateUuid = caCertificate.isPresent() ? caCertificate.get().getUuid() : null; + // If CRL is not present or current UTC time is past its next_update timestamp, download the CRL and save the CRL and its entries in database + if (crlOptional.isEmpty() || (crl = crlOptional.get()).getNextUpdate().before(new Date())) { + Crl newCrl = createCrlAndCrlEntries(crlDistributionPoints, issuerDn, issuerSerialNumber, caCertificateUuid, crl != null ? crl.getCrlNumber() : null); + // If CRL received is not null, then the downloaded CRL is updated CRL, delete old CRL and use updated one + if (newCrl != null) { + if (crlOptional.isPresent()) crlRepository.delete(crl); + crl = newCrl; + } + } + + // Check if certificate has freshestCrl extension set + if (certificate.getExtensionValue(Extension.freshestCRL.getId()) != null) { + // If no delta CRL is set or delta CRL is not up-to-date, download delta CRL + if (crl != null && (crl.getNextUpdateDelta() == null || !crl.getNextUpdateDelta().before(new Date()))) { + updateCrlAndCrlEntriesFromDeltaCrl(certificate, crl, issuerDn, issuerSerialNumber, caCertificateUuid); + } + } + return crl; + } + + @Override + public CrlEntry findCrlEntryForCertificate(String serialNumber, UUID crlUuid) { + CrlEntryId crlEntryId = new CrlEntryId(crlUuid, serialNumber); + return crlEntryRepository.findById(crlEntryId).orElse(null); + } + + private void updateDeltaCrl(Crl crl, X509CRL deltaCrl) throws IOException { + ASN1Primitive encodedCrlNumber = JcaX509ExtensionUtils.parseExtensionValue(deltaCrl.getExtensionValue(Extension.cRLNumber.getId())); + // If delta CRL number has been set, check if delta CRL number is greater than one in DB entity, if it is, process delta CRL entries + if (crl.getCrlNumberDelta() == null || Integer.parseInt(encodedCrlNumber.toString()) > Integer.parseInt(crl.getCrlNumberDelta())) { + List crlEntries = crl.getCrlEntries(); + Date lastRevocationDateNew = crl.getLastRevocationDate(); + Set deltaCrlEntries = deltaCrl.getRevokedCertificates(); + if (deltaCrlEntries != null) { + Map crlEntryMap = crl.getCrlEntriesMap(); + for (X509CRLEntry deltaCrlEntry : deltaCrlEntries) { + Date entryRevocationDate = deltaCrlEntry.getRevocationDate(); + // Process only entries which revocation date is >= last_revocation_date, others are already in DB + if (entryRevocationDate.after(crl.getLastRevocationDate()) || entryRevocationDate.equals(crl.getLastRevocationDate())) { + String serialNumber = String.valueOf(deltaCrlEntry.getSerialNumber()); + CrlEntry crlEntry = crlEntryMap.get(serialNumber); + // Entry by serial number is not present, add new one + if (crlEntry == null) { + CrlEntry crlEntryNew = createCrlEntry(deltaCrlEntry, crl); + crlEntries.add(crlEntryNew); + // Entry by serial number is present and revocation reason is REMOVE_FROM_CRL, remove this entry + } else if (Objects.equals(deltaCrlEntry.getRevocationReason(), CRLReason.REMOVE_FROM_CRL)) { + crlEntries.remove(crlEntry); + crlEntryRepository.delete(crlEntry); + // Entry by serial number is present, probably reason changed so update its revocation reason and date + } else { + crlEntry.setRevocationReason(deltaCrlEntry.getRevocationReason() == null ? CertificateRevocationReason.UNSPECIFIED : CertificateRevocationReason.fromCrlReason(deltaCrlEntry.getRevocationReason())); + crlEntry.setRevocationDate(deltaCrlEntry.getRevocationDate()); + crlEntryRepository.save(crlEntry); + } + if (lastRevocationDateNew.before(deltaCrlEntry.getRevocationDate())) + lastRevocationDateNew = deltaCrlEntry.getRevocationDate(); + } + } + } + // Update last revocation date from new/updated entries + crl.setLastRevocationDate(lastRevocationDateNew); + crl.setCrlNumberDelta(encodedCrlNumber.toString()); + crl.setNextUpdateDelta(deltaCrl.getNextUpdate()); + crlRepository.save(crl); + } + } + + + private CrlEntry createCrlEntry(X509CRLEntry x509CRLEntry, Crl crl) { + CrlEntry crlEntry = new CrlEntry(); + crlEntry.setCrl(crl); + crlEntry.getId().setSerialNumber(x509CRLEntry.getSerialNumber().toString(16)); + crlEntry.getId().setCrlUuid(crl.getUuid()); + crlEntry.setRevocationDate(x509CRLEntry.getRevocationDate()); + crlEntry.setRevocationReason(x509CRLEntry.getRevocationReason() == null ? CertificateRevocationReason.UNSPECIFIED : CertificateRevocationReason.fromCrlReason(x509CRLEntry.getRevocationReason())); + crlEntry.setCrl(crl); + crlEntryRepository.save(crlEntry); + return crlEntry; + } + +} diff --git a/src/main/java/com/czertainly/core/service/impl/CryptographicKeyServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/CryptographicKeyServiceImpl.java index 8999ea4d2..27754d94d 100644 --- a/src/main/java/com/czertainly/core/service/impl/CryptographicKeyServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/CryptographicKeyServiceImpl.java @@ -846,7 +846,8 @@ private void createKeyAndItems(UUID connectorUuid, TokenInstanceReference tokenI item.getKeyData(), cryptographicKey, connectorUuid, - true + true, + false ) ); } @@ -881,7 +882,7 @@ private CryptographicKey createKeyEntity(KeyRequestDto request, TokenProfile tok return key; } - private CryptographicKeyItem createKeyContent(String referenceUuid, String referenceName, KeyData keyData, CryptographicKey cryptographicKey, UUID connectorUuid, boolean isDiscovered) { + private CryptographicKeyItem createKeyContent(String referenceUuid, String referenceName, KeyData keyData, CryptographicKey cryptographicKey, UUID connectorUuid, boolean isDiscovered, boolean enabled) { logger.info("Creating the Key Content for {}", cryptographicKey); CryptographicKeyItem content = new CryptographicKeyItem(); content.setName(referenceName); @@ -893,7 +894,7 @@ private CryptographicKeyItem createKeyContent(String referenceUuid, String refer content.setLength(keyData.getLength()); content.setKeyReferenceUuid(UUID.fromString(referenceUuid)); content.setState(KeyState.ACTIVE); - content.setEnabled(false); + content.setEnabled(enabled); if (cryptographicKey.getTokenProfile() != null) { content.setUsage( cryptographicKey @@ -1020,6 +1021,7 @@ private List mergeAndValidateAttributes(KeyRequestType type, Toke } private CryptographicKey createKeyTypeOfKeyPair(Connector connector, TokenProfile tokenProfile, KeyRequestDto request, CreateKeyRequestDto createKeyRequestDto, List attributes) throws ConnectorException { + boolean enabled = Boolean.TRUE.equals(request.getEnabled()); KeyPairDataResponseDto response = keyManagementApiClient.createKeyPair( connector.mapToDto(), tokenProfile.getTokenInstanceReference().getTokenInstanceUuid(), @@ -1039,7 +1041,8 @@ private CryptographicKey createKeyTypeOfKeyPair(Connector connector, TokenProfil response.getPrivateKeyData().getKeyData(), key, connector.getUuid(), - false + false, + enabled )); children.add(createKeyContent( response.getPublicKeyData().getUuid(), @@ -1047,7 +1050,8 @@ private CryptographicKey createKeyTypeOfKeyPair(Connector connector, TokenProfil response.getPublicKeyData().getKeyData(), key, connector.getUuid(), - false + false, + enabled )); key.setItems(children); try { @@ -1082,7 +1086,9 @@ private CryptographicKey createKeyTypeOfSecret(Connector connector, TokenProfile response.getKeyData(), key, connector.getUuid(), - false + false, + Boolean.TRUE.equals(request.getEnabled() + ) ) ) ); diff --git a/src/main/java/com/czertainly/core/service/impl/DiscoveryServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/DiscoveryServiceImpl.java index dff53bc1e..8694128f4 100644 --- a/src/main/java/com/czertainly/core/service/impl/DiscoveryServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/DiscoveryServiceImpl.java @@ -272,8 +272,9 @@ public void createDiscovery(DiscoveryHistory modal) { modal.setStatus(DiscoveryStatus.WARNING); modal.setMessage( "Discovery exceeded maximum time of " + MAXIMUM_WAIT_TIME / (60 * 60) + " hours. There are no changes in number of certificates discovered. Please abort the discovery if the provider is stuck in IN_PROGRESS"); + discoveryRepository.save(modal); } - discoveryRepository.save(modal); + oldCertificateCount = response.getTotalCertificatesDiscovered(); waitForCompletion = checkForCompletion(response); } @@ -285,6 +286,11 @@ public void createDiscovery(DiscoveryHistory modal) { getRequest.setPageNumber(currentPage); getRequest.setItemsPerPage(MAXIMUM_CERTIFICATES_PER_PAGE); response = discoveryApiClient.getDiscoveryData(connector.mapToDto(), getRequest, response.getUuid()); + + if(response.getCertificateData().isEmpty()) { + modal.setMessage(String.format("Retrieved only %d certificates but provider discovered %d certificates in total.", currentTotal, response.getTotalCertificatesDiscovered())); + break; + } if (response.getCertificateData().size() > MAXIMUM_CERTIFICATES_PER_PAGE) { response.setStatus(DiscoveryStatus.FAILED); updateDiscovery(modal, response); diff --git a/src/main/java/com/czertainly/core/service/impl/NotificationServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/NotificationServiceImpl.java index edb5a409d..4b399978b 100644 --- a/src/main/java/com/czertainly/core/service/impl/NotificationServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/NotificationServiceImpl.java @@ -10,6 +10,8 @@ import com.czertainly.core.dao.entity.Notification; import com.czertainly.core.dao.entity.NotificationRecipient; import com.czertainly.core.dao.repository.NotificationRepository; +import com.czertainly.core.security.authn.client.RoleManagementApiClient; +import com.czertainly.core.security.authn.client.UserManagementApiClient; import com.czertainly.core.security.authz.SecuredUUID; import com.czertainly.core.service.NotificationService; import com.czertainly.core.service.RoleManagementService; @@ -39,10 +41,10 @@ public class NotificationServiceImpl implements NotificationService { NotificationRepository notificationRepository; @Autowired - UserManagementService userService; + private UserManagementApiClient userManagementApiClient; @Autowired - RoleManagementService roleService; + private RoleManagementApiClient roleManagementApiClient; @Override public NotificationDto createNotificationForUser(String message, String detail, String userUuid, Resource target, String targetUuids) throws ValidationException { @@ -77,12 +79,12 @@ public NotificationDto createNotificationForUsers(String message, String detail, @Override public NotificationDto createNotificationForGroup(String message, String detail, String groupUuid, Resource target, String targetUuids) throws ValidationException { - return createNotificationForUsers(message, detail, userService.listUsers().stream().filter(u -> groupUuid.equals(u.getGroupUuid())).map(UserDto::getUuid).collect(Collectors.toList()), target, targetUuids); + return createNotificationForUsers(message, detail, userManagementApiClient.getUsers().getData().stream().filter(u -> groupUuid.equals(u.getGroupUuid())).map(UserDto::getUuid).toList(), target, targetUuids); } @Override public NotificationDto createNotificationForRole(String message, String detail, String roleUuid, Resource target, String targetUuids) throws ValidationException { - return createNotificationForUsers(message, detail, roleService.getRoleUsers(roleUuid).stream().map(UserDto::getUuid).collect(Collectors.toList()), target, targetUuids); + return createNotificationForUsers(message, detail, roleManagementApiClient.getRoleUsers(roleUuid).stream().map(UserDto::getUuid).toList(), target, targetUuids); } @Override diff --git a/src/main/java/com/czertainly/core/service/impl/RaProfileServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/RaProfileServiceImpl.java index d56c64070..4dbc42dcd 100644 --- a/src/main/java/com/czertainly/core/service/impl/RaProfileServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/RaProfileServiceImpl.java @@ -10,9 +10,13 @@ import com.czertainly.api.model.common.NameAndUuidDto; import com.czertainly.api.model.common.attribute.v2.BaseAttribute; import com.czertainly.api.model.common.attribute.v2.DataAttribute; +import com.czertainly.api.model.connector.authority.CaCertificatesRequestDto; +import com.czertainly.api.model.connector.authority.CaCertificatesResponseDto; +import com.czertainly.api.model.connector.v2.CertificateDataResponseDto; import com.czertainly.api.model.core.audit.ObjectType; import com.czertainly.api.model.core.audit.OperationType; import com.czertainly.api.model.core.auth.Resource; +import com.czertainly.api.model.core.certificate.CertificateDetailDto; import com.czertainly.api.model.core.raprofile.RaProfileDto; import com.czertainly.core.aop.AuditLogged; import com.czertainly.core.dao.entity.*; @@ -32,6 +36,8 @@ import com.czertainly.core.service.model.SecuredList; import com.czertainly.core.service.v2.ExtendedAttributeService; import com.czertainly.core.util.AttributeDefinitionUtils; +import com.czertainly.core.util.CertificateUtil; +import com.czertainly.core.util.X509ObjectToString; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -44,6 +50,9 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -69,6 +78,7 @@ public class RaProfileServiceImpl implements RaProfileService { private ScepProfileRepository scepProfileRepository; private ApprovalProfileRelationRepository approvalProfileRelationRepository; private ApprovalProfileRepository approvalProfileRepository; + private CertificateContentRepository certificateContentRepository; @Override @@ -105,13 +115,17 @@ public RaProfileDto addRaProfile(SecuredParentUUID authorityInstanceUuid, AddRaP List attributes = mergeAndValidateAttributes(authorityInstanceRef, dto.getAttributes()); RaProfile raProfile = createRaProfile(dto, attributes, authorityInstanceRef); + raProfileRepository.save(raProfile); + setAuthorityCertificates(authorityInstanceRef, raProfile); + attributeService.createAttributeContent(raProfile.getUuid(), dto.getCustomAttributes(), Resource.RA_PROFILE); RaProfileDto raProfileDto = raProfile.mapToDto(); raProfileDto.setCustomAttributes(attributeService.getCustomAttributesWithValues(raProfile.getUuid(), Resource.RA_PROFILE)); + return raProfileDto; } @@ -497,6 +511,50 @@ public void disassociateApprovalProfile(String authorityInstanceUuid, String raP approvalProfileRelationRepository.deleteAll(approvalProfileRelations); } + @Override + @ExternalAuthorization(resource = Resource.RA_PROFILE, action = ResourceAction.DETAIL, parentResource = Resource.AUTHORITY, parentAction = ResourceAction.DETAIL) + public List getAuthorityCertificateChain(SecuredParentUUID authorityUuid, SecuredUUID raProfileUuid) throws ConnectorException { + RaProfile raProfile = getRaProfileEntity(raProfileUuid); + List requestAttributeDtos = AttributeDefinitionUtils.deserializeRequestAttributes(raProfile.getAttributes()); + AuthorityInstanceReference authorityInstanceReference = authorityInstanceReferenceRepository.findByUuid(authorityUuid) + .orElseThrow(() -> new NotFoundException(AuthorityInstanceReference.class, authorityUuid)); + CaCertificatesResponseDto caCertificatesResponseDto = authorityInstanceApiClient.getCaCertificates(authorityInstanceReference.getConnector().mapToDto(), authorityInstanceReference.getAuthorityInstanceUuid(), new CaCertificatesRequestDto(requestAttributeDtos)); + List certificateDataResponseDtos = caCertificatesResponseDto.getCertificates(); + List certificateDetailDtos = new ArrayList<>(); + for (CertificateDataResponseDto certificateDataResponseDto : certificateDataResponseDtos) { + X509Certificate certificate; + String fingerprint; + try { + certificate = CertificateUtil.parseCertificate(certificateDataResponseDto.getCertificateData()); + fingerprint = CertificateUtil.getThumbprint(certificate); + } catch (java.security.cert.CertificateException | NoSuchAlgorithmException e) { + logger.warn("Cannot process certificate from CA certificate chain returned from authority of RA profile {}", raProfile.getName()); + break; + } + + Optional existingCertificate = certificateRepository.findByFingerprint(fingerprint); + if (existingCertificate.isPresent()) { + certificateDetailDtos.add(existingCertificate.get().mapToDto()); + } else { + Certificate modal = new Certificate(); + CertificateUtil.prepareIssuedCertificate(modal, certificate); + CertificateContent certificateContent = certificateContentRepository.findByFingerprint(fingerprint); + if (certificateContent == null) { + certificateContent = new CertificateContent(); + certificateContent.setContent(CertificateUtil.normalizeCertificateContent(X509ObjectToString.toPem(certificate))); + certificateContent.setFingerprint(fingerprint); + certificateContentRepository.save(certificateContent); + } + modal.setFingerprint(fingerprint); + modal.setCertificateContent(certificateContent); + modal.setCertificateContentId(certificateContent.getId()); + certificateRepository.save(modal); + certificateDetailDtos.add(modal.mapToDto()); + } + } + return certificateDetailDtos; + } + private List mergeAndValidateAttributes(AuthorityInstanceReference authorityInstanceRef, List attributes) throws ConnectorException { List definitions = authorityInstanceApiClient.listRAProfileAttributes( authorityInstanceRef.getConnector().mapToDto(), @@ -544,6 +602,8 @@ private RaProfile updateRaProfile(RaProfile entity, AuthorityInstanceReference a entity.setEnabled(dto.isEnabled() != null && dto.isEnabled()); } entity.setAuthorityInstanceName(authorityInstanceRef.getName()); + + setAuthorityCertificates(authorityInstanceRef, entity); return entity; } @@ -572,6 +632,18 @@ private List getComplianceProfilesForRaProfile(S .map(ComplianceProfile::raProfileMapToDto).toList(); } + private void setAuthorityCertificates(AuthorityInstanceReference authorityInstanceRef, RaProfile raProfile) { + try { + List certificateChain = getAuthorityCertificateChain(SecuredParentUUID.fromUUID(authorityInstanceRef.getUuid()), SecuredUUID.fromUUID(raProfile.getUuid())); + raProfile.setAuthorityCertificateUuid(certificateChain.isEmpty() ? null : UUID.fromString(certificateChain.get(0).getUuid())); + } catch (NotFoundException ignored) { + // exception ignored since get CA certs from connector is optional + logger.debug("CA certificate chain not implemented for connector {}: {}", + authorityInstanceRef.getConnector().getName(), authorityInstanceRef.getConnector().getUuid()); + } catch (Exception e) { + logger.warn("CA certificate chain from RA profile authority could not be retrieved: {}", e.getMessage()); + } + } // SETTERs @@ -595,6 +667,7 @@ public void setCertificateRepository(CertificateRepository certificateRepository this.certificateRepository = certificateRepository; } + @Autowired public void setAcmeProfileRepository(AcmeProfileRepository acmeProfileRepository) { this.acmeProfileRepository = acmeProfileRepository; @@ -644,4 +717,10 @@ public void setApprovalProfileRelationRepository(ApprovalProfileRelationReposito public void setApprovalProfileRepository(ApprovalProfileRepository approvalProfileRepository) { this.approvalProfileRepository = approvalProfileRepository; } + + @Autowired + public void setCertificateContentRepository(CertificateContentRepository certificateContentRepository) { + this.certificateContentRepository = certificateContentRepository; + } + } diff --git a/src/main/java/com/czertainly/core/service/impl/RoleManagementServiceImpl.java b/src/main/java/com/czertainly/core/service/impl/RoleManagementServiceImpl.java index 987917290..8108b391a 100644 --- a/src/main/java/com/czertainly/core/service/impl/RoleManagementServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/impl/RoleManagementServiceImpl.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import jakarta.transaction.Transactional; + import java.util.List; import java.util.UUID; @@ -134,11 +135,13 @@ public void removeResourcePermissionObjects(String roleUuid, String resourceUuid } @Override + @ExternalAuthorization(resource = Resource.ROLE, action = ResourceAction.DETAIL) public List getRoleUsers(String roleUuid) { return roleManagementApiClient.getRoleUsers(roleUuid); } @Override + @ExternalAuthorization(resource = Resource.ROLE, action = ResourceAction.UPDATE) public RoleDetailDto updateUsers(String roleUuid, List userUuids) { return roleManagementApiClient.updateUsers(roleUuid, userUuids); } diff --git a/src/main/java/com/czertainly/core/service/v2/impl/ClientOperationServiceImpl.java b/src/main/java/com/czertainly/core/service/v2/impl/ClientOperationServiceImpl.java index 6af9d4d3b..f3b03c342 100644 --- a/src/main/java/com/czertainly/core/service/v2/impl/ClientOperationServiceImpl.java +++ b/src/main/java/com/czertainly/core/service/v2/impl/ClientOperationServiceImpl.java @@ -681,6 +681,7 @@ public void revokeCertificateAction(final UUID certificateUuid, ClientCertificat caRequest); certificate.setState(CertificateState.REVOKED); + certificate.setRevokeAttributes(AttributeDefinitionUtils.serializeRequestAttributes(request.getAttributes())); certificateRepository.save(certificate); certificateEventHistoryService.addEventHistory(certificate.getUuid(), CertificateEvent.REVOKE, CertificateEventStatus.SUCCESS, "Certificate revoked. Reason: " + caRequest.getReason().getLabel(), ""); } catch (Exception e) { @@ -692,21 +693,13 @@ public void revokeCertificateAction(final UUID certificateUuid, ClientCertificat throw new CertificateOperationException("Failed to revoke certificate: " + e.getMessage()); } - try { - CertificateValidationStatus oldStatus = certificate.getValidationStatus(); - certificate.setValidationStatus(CertificateValidationStatus.REVOKED); - certificate.setRevokeAttributes(AttributeDefinitionUtils.serialize(extendedAttributeService.mergeAndValidateIssueAttributes(raProfile, request.getAttributes()))); - logger.debug("Certificate revoked. Proceeding to check and destroy key"); - - if (certificate.getKey() != null && request.isDestroyKey()) { + if (certificate.getKey() != null && request.isDestroyKey()) { + try { + logger.debug("Certificate revoked. Proceeding to check and destroy key"); keyService.destroyKey(List.of(certificate.getKeyUuid().toString())); + } catch (Exception e) { + logger.warn("Failed to destroy certificate key: {}", e.getMessage()); } - certificateRepository.save(certificate); - - eventProducer.produceCertificateStatusChangeEventMessage(certificate.getUuid(), CertificateEvent.UPDATE_VALIDATION_STATUS, CertificateEventStatus.SUCCESS, oldStatus, CertificateValidationStatus.REVOKED); - notificationProducer.produceNotificationCertificateStatusChanged(oldStatus, CertificateValidationStatus.REVOKED, certificate.mapToListDto()); - } catch (Exception e) { - logger.warn(e.getMessage()); } // notify diff --git a/src/main/java/com/czertainly/core/tasks/DiscoveryCertificateTask.java b/src/main/java/com/czertainly/core/tasks/DiscoveryCertificateTask.java index f693d3beb..84eea96b8 100644 --- a/src/main/java/com/czertainly/core/tasks/DiscoveryCertificateTask.java +++ b/src/main/java/com/czertainly/core/tasks/DiscoveryCertificateTask.java @@ -36,7 +36,7 @@ public class DiscoveryCertificateTask extends SchedulerJobProcessor { private ObjectMapper mapper = new ObjectMapper(); - private final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmm"); + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss.FFF"); @Autowired public void setDiscoveryService(DiscoveryService discoveryService) { diff --git a/src/main/java/com/czertainly/core/util/CertificateUtil.java b/src/main/java/com/czertainly/core/util/CertificateUtil.java index cf96b7917..11e1ee9cb 100644 --- a/src/main/java/com/czertainly/core/util/CertificateUtil.java +++ b/src/main/java/com/czertainly/core/util/CertificateUtil.java @@ -355,8 +355,10 @@ public static void prepareIssuedCertificate(Certificate modal, X509Certificate c } modal.setKeyUsage( MetaDefinitions.serializeArrayString(CertificateUtil.keyUsageExtractor(certificate.getKeyUsage()))); - modal.setBasicConstraints(CertificateUtil.getBasicConstraint(certificate.getBasicConstraints())); + String basicConstraints = CertificateUtil.getBasicConstraint(certificate.getBasicConstraints()); + modal.setBasicConstraints(basicConstraints); + if (basicConstraints.equals("Subject Type=CA")) modal.setTrustedCa(false); } diff --git a/src/main/java/com/czertainly/core/util/CrlUtil.java b/src/main/java/com/czertainly/core/util/CrlUtil.java index d8ea6ce59..8fb766e85 100644 --- a/src/main/java/com/czertainly/core/util/CrlUtil.java +++ b/src/main/java/com/czertainly/core/util/CrlUtil.java @@ -4,45 +4,37 @@ import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.DEROctetString; -import org.bouncycastle.asn1.x509.CRLDistPoint; -import org.bouncycastle.asn1.x509.DistributionPoint; -import org.bouncycastle.asn1.x509.DistributionPointName; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.DataInputStream; -import java.io.FileNotFoundException; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; -import java.security.GeneralSecurityException; -import java.security.cert.*; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.security.cert.CRLException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; import java.util.ArrayList; import java.util.List; public class CrlUtil { private static final Logger logger = LoggerFactory.getLogger(CrlUtil.class); //CRL Timeout setting when initiating URL Connection. If the connection takes more than 30 seconds, it is determined as not reachable - private static final Integer CRL_CONNECTION_TIMEOUT = 30; //seconds + private static final Integer CRL_CONNECTION_TIMEOUT = 1000; //milliseconds private CrlUtil() { } - public static List getCDPFromCertificate(X509Certificate certificate) throws IOException { - byte[] crlDistributionPointDerEncodedArray = certificate - .getExtensionValue(Extension.cRLDistributionPoints.getId()); - if (crlDistributionPointDerEncodedArray == null) { + public static List getCDPFromCertificate(byte[] extensionToDownloadFrom) throws IOException { + + if (extensionToDownloadFrom == null) { return new ArrayList<>(); } ASN1InputStream oAsnInStream = new ASN1InputStream( - new ByteArrayInputStream(crlDistributionPointDerEncodedArray)); + new ByteArrayInputStream(extensionToDownloadFrom)); ASN1Primitive derObjCrlDP = oAsnInStream.readObject(); DEROctetString dosCrlDP = (DEROctetString) derObjCrlDP; @@ -62,9 +54,9 @@ public static List getCDPFromCertificate(X509Certificate certificate) th if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME) { GeneralName[] genNames = GeneralNames.getInstance(dpn.getName()).getNames(); // Look for an URI - for (int j = 0; j < genNames.length; j++) { - if (genNames[j].getTagNo() == GeneralName.uniformResourceIdentifier) { - String url = DERIA5String.getInstance(genNames[j].getName()).getString(); + for (GeneralName genName : genNames) { + if (genName.getTagNo() == GeneralName.uniformResourceIdentifier) { + String url = DERIA5String.getInstance(genName.getName()).getString(); crlUrls.add(url); } } @@ -73,27 +65,19 @@ public static List getCDPFromCertificate(X509Certificate certificate) th return crlUrls; } - public static String checkCertificateRevocationList(X509Certificate certificate, String crlUrl) throws IOException, CertificateException, CRLException { - X509CRL crl; + public static X509CRL getX509Crl(String crlUrl) throws IOException, CertificateException { + X509CRL X509Crl; URL url = new URL(crlUrl); URLConnection connection = url.openConnection(); connection.setConnectTimeout(CRL_CONNECTION_TIMEOUT); CertificateFactory cf = CertificateFactory.getInstance("X509"); try (DataInputStream inStream = new DataInputStream(connection.getInputStream())) { - crl = (X509CRL) cf.generateCRL(inStream); - } catch (FileNotFoundException e) { + X509Crl = (X509CRL) cf.generateCRL(inStream); + } catch (CRLException e) { throw new CertificateException("File " + e.getMessage() + " not found"); } - X509CRLEntry crlCertificate = crl.getRevokedCertificate(certificate.getSerialNumber()); - if (crlCertificate == null) { - return null; - } else { - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); - String strDate = dateFormat.format(crlCertificate.getRevocationDate()); - String reason = crlCertificate.getRevocationReason() != null ? crlCertificate.getRevocationReason().toString() : "Unspecified"; - - return String.format("Reason: %s. Date: %s", reason, strDate); - } + return X509Crl; } + } diff --git a/src/main/java/com/czertainly/core/util/converter/CertificateFormatEncodingConverter.java b/src/main/java/com/czertainly/core/util/converter/CertificateFormatEncodingConverter.java new file mode 100644 index 000000000..58cda1a09 --- /dev/null +++ b/src/main/java/com/czertainly/core/util/converter/CertificateFormatEncodingConverter.java @@ -0,0 +1,11 @@ +package com.czertainly.core.util.converter; + +import com.czertainly.api.model.core.certificate.CertificateFormatEncoding; + +import java.beans.PropertyEditorSupport; + +public class CertificateFormatEncodingConverter extends PropertyEditorSupport { + public void setAsText(final String text) throws IllegalArgumentException { + setValue(CertificateFormatEncoding.fromCode(text)); + } +} \ No newline at end of file diff --git a/src/main/java/com/czertainly/core/validation/certificate/X509CertificateValidator.java b/src/main/java/com/czertainly/core/validation/certificate/X509CertificateValidator.java index 3df04b11a..c90a0de79 100644 --- a/src/main/java/com/czertainly/core/validation/certificate/X509CertificateValidator.java +++ b/src/main/java/com/czertainly/core/validation/certificate/X509CertificateValidator.java @@ -1,21 +1,31 @@ package com.czertainly.core.validation.certificate; +import com.czertainly.api.exception.ValidationException; import com.czertainly.api.model.core.certificate.CertificateState; import com.czertainly.api.model.core.certificate.CertificateValidationCheck; import com.czertainly.api.model.core.certificate.CertificateValidationCheckDto; import com.czertainly.api.model.core.certificate.CertificateValidationStatus; import com.czertainly.core.dao.entity.Certificate; +import com.czertainly.core.dao.entity.Crl; +import com.czertainly.core.dao.entity.CrlEntry; import com.czertainly.core.dao.repository.CertificateRepository; +import com.czertainly.core.dao.repository.CrlEntryRepository; +import com.czertainly.core.dao.repository.CrlRepository; +import com.czertainly.core.service.CrlService; import com.czertainly.core.util.CertificateUtil; import com.czertainly.core.util.CrlUtil; +import com.czertainly.core.util.CzertainlyX500NameStyle; import com.czertainly.core.util.OcspUtil; import com.fasterxml.jackson.databind.ObjectMapper; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; +import java.security.cert.CRLException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Instant; @@ -28,14 +38,22 @@ public class X509CertificateValidator implements ICertificateValidator { private static final Logger logger = LoggerFactory.getLogger(X509CertificateValidator.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final int DAYS_TO_EXPIRE = 30; - private CertificateRepository certificateRepository; + + private CrlService crlService; + @Autowired public void setCertificateRepository(CertificateRepository certificateRepository) { this.certificateRepository = certificateRepository; } + @Autowired + public void setCrlService(CrlService crlService) { + this.crlService = crlService; + } + + @Override public CertificateValidationStatus validateCertificate(Certificate certificate, boolean isCompleteChain) throws CertificateException { logger.debug("Initiating the certificate validation: {}", certificate); @@ -56,7 +74,7 @@ public CertificateValidationStatus validateCertificate(Certificate certificate, x509Certificate = CertificateUtil.getX509Certificate(certificateChain.get(i).getCertificateContent().getContent()); boolean isEndCertificate = i == 0; - validationOutput = validatePathCertificate(x509Certificate, x509IssuerCertificate, previousCertStatus, isCompleteChain, isEndCertificate); + validationOutput = validatePathCertificate(x509Certificate, x509IssuerCertificate, certificateChain.get(i).getTrustedCa(), previousCertStatus, isCompleteChain, isEndCertificate, certificateChain.get(i).getIssuerDnNormalized()); CertificateValidationStatus resultStatus = calculateResultStatus(validationOutput); finalizeValidation(certificateChain.get(i), resultStatus, validationOutput); @@ -68,7 +86,7 @@ public CertificateValidationStatus validateCertificate(Certificate certificate, return previousCertStatus; } - private Map validatePathCertificate(X509Certificate certificate, X509Certificate issuerCertificate, CertificateValidationStatus issuerCertificateStatus, boolean isCompleteChain, boolean isEndCertificate) { + private Map validatePathCertificate(X509Certificate certificate, X509Certificate issuerCertificate, Boolean trustedCa, CertificateValidationStatus issuerCertificateStatus, boolean isCompleteChain, boolean isEndCertificate, String issuerDn) { Map validationOutput = initializeValidationOutput(); // check certificate signature @@ -82,11 +100,11 @@ private Map validateP // check if certificate is not revoked - OCSP & CRL // section (a)(3) in https://datatracker.ietf.org/doc/html/rfc5280#section-6.1.3 validationOutput.put(CertificateValidationCheck.OCSP_VERIFICATION, checkOcspRevocationStatus(certificate, issuerCertificate)); - validationOutput.put(CertificateValidationCheck.CRL_VERIFICATION, checkCrlRevocationStatus(certificate, issuerCertificate)); + validationOutput.put(CertificateValidationCheck.CRL_VERIFICATION, checkCrlRevocationStatus(certificate, issuerCertificate, isCompleteChain)); // check certificate issuer DN and if certificate chain is valid // section (a)(4) in https://datatracker.ietf.org/doc/html/rfc5280#section-6.1.3 - validationOutput.put(CertificateValidationCheck.CERTIFICATE_CHAIN, checkCertificateChain(certificate, issuerCertificate, issuerCertificateStatus, isCompleteChain)); + validationOutput.put(CertificateValidationCheck.CERTIFICATE_CHAIN, checkCertificateChain(certificate, issuerCertificate, trustedCa, issuerCertificateStatus, isCompleteChain)); // (k) and (l) section in https://datatracker.ietf.org/doc/html/rfc5280#section-6.1.4 validationOutput.put(CertificateValidationCheck.BASIC_CONSTRAINTS, checkBasicConstraints(certificate, issuerCertificate, isEndCertificate)); @@ -97,11 +115,15 @@ private Map validateP return validationOutput; } - private CertificateValidationCheckDto checkCertificateChain(X509Certificate certificate, X509Certificate issuerCertificate, CertificateValidationStatus issuerCertificateStatus, boolean isCompleteChain) { + private CertificateValidationCheckDto checkCertificateChain(X509Certificate certificate, X509Certificate issuerCertificate, Boolean isTrustedCa, CertificateValidationStatus issuerCertificateStatus, boolean isCompleteChain) { if (issuerCertificate == null) { // should be trust anchor (Root CA certificate) if (isCompleteChain) { - return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.VALID, "Certificate chain is complete. Certificate is Root CA certificate (trusted anchor)."); + if (Boolean.TRUE.equals(isTrustedCa)) { + return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.VALID, "Certificate chain is complete. Certificate is trusted root CA certificate."); + } else { + return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.INVALID, "Certificate chain is complete. Certificate is root CA certificate but not marked as trusted."); + } } else { return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.INVALID, "Incomplete certificate chain. Issuer certificate is not available in the inventory or in the AIA extension."); } @@ -117,13 +139,15 @@ private CertificateValidationCheckDto checkCertificateChain(X509Certificate cert } if (isCompleteChain) { - if (issuerNameEqualityMessage.isEmpty() && issuerStatusMessage.isEmpty()) { - return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.VALID, "Certificate chain is complete."); - } else { - return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.INVALID, "Certificate chain is complete. " + issuerNameEqualityMessage + issuerStatusMessage); + String trustedCaMessage = ""; + if (isTrustedCa != null) { + trustedCaMessage = Boolean.TRUE.equals(isTrustedCa) ? " Certificate is trusted intermediate CA." : " Certificate is intermediate CA certificate but not marked as trusted."; } + + CertificateValidationStatus chainValidationStatus = issuerNameEqualityMessage.isEmpty() && issuerStatusMessage.isEmpty() && (trustedCaMessage.isEmpty() || Boolean.TRUE.equals(isTrustedCa)) ? CertificateValidationStatus.VALID : CertificateValidationStatus.INVALID; + return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, chainValidationStatus, String.format("Certificate chain is complete.%s%s%s", trustedCaMessage, issuerNameEqualityMessage, issuerStatusMessage)); } else { - return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.INVALID, "Incomplete certificate chain. Missing certificate in validation path." + issuerNameEqualityMessage + issuerStatusMessage); + return new CertificateValidationCheckDto(CertificateValidationCheck.CERTIFICATE_CHAIN, CertificateValidationStatus.INVALID, String.format("Incomplete certificate chain. Missing certificate in validation path.%s%s", issuerNameEqualityMessage, issuerStatusMessage)); } } } @@ -218,58 +242,49 @@ private CertificateValidationCheckDto checkOcspRevocationStatus(X509Certificate return new CertificateValidationCheckDto(CertificateValidationCheck.OCSP_VERIFICATION, ocspOutputStatus, ocspMessage.toString()); } - private CertificateValidationCheckDto checkCrlRevocationStatus(X509Certificate certificate, X509Certificate issuerCertificate) { + private CertificateValidationCheckDto checkCrlRevocationStatus(X509Certificate certificate, X509Certificate issuerCertificate, boolean isCompleteChain) { if (issuerCertificate == null) { - return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.NOT_CHECKED, "Issuer certificate is not available."); + if (!isCompleteChain) + return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.NOT_CHECKED, "Issuer certificate is not available."); + issuerCertificate = certificate; } - List crlUrls; + if (certificate.getExtensionValue(Extension.cRLDistributionPoints.getId()) == null) { + return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.NOT_CHECKED, "The cRLDistributionPoints extension is not set."); + } + Crl crl; try { - crlUrls = CrlUtil.getCDPFromCertificate(certificate); + crl = crlService.getCurrentCrl(certificate, issuerCertificate); } catch (IOException e) { return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.FAILED, "Failed to retrieve CRL URL from certificate: " + e.getMessage()); + } catch (ValidationException e) { + return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.FAILED, "Failed to process CRL: " + e.getMessage()); } - if (crlUrls.isEmpty()) { - return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.NOT_CHECKED, "No CRL URL in certificate"); + if (crl == null) { + return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, CertificateValidationStatus.NOT_CHECKED, "No available working CRL URL found in cRLDistributionPoints extension."); } - String crlOutput = ""; StringBuilder crlMessage = new StringBuilder(); - CertificateValidationStatus crlOutputStatus = CertificateValidationStatus.NOT_CHECKED; - for (String crlUrl : crlUrls) { - try { - crlOutput = CrlUtil.checkCertificateRevocationList(certificate, crlUrl); - if (crlOutput == null) { - if (crlOutputStatus.equals(CertificateValidationStatus.NOT_CHECKED)) { - crlOutputStatus = CertificateValidationStatus.VALID; - } - crlMessage.append("CRL verification successful from URL "); - crlMessage.append(crlUrl); - crlMessage.append(". "); - } else { - crlOutputStatus = CertificateValidationStatus.REVOKED; - crlMessage.append("Certificate was revoked according to information from CRL URL "); - crlMessage.append(crlUrl); - crlMessage.append(". "); - crlMessage.append(crlOutput); - crlMessage.append(". "); - break; - } - } catch (Exception e) { - logger.debug("Not able to check CRL: {}", e.getMessage()); - crlOutputStatus = CertificateValidationStatus.FAILED; - crlMessage.append("Error while checking CRL URL "); - crlMessage.append(crlUrl); - crlMessage.append(". Error: "); - crlMessage.append(e.getMessage()); - crlMessage.append(". "); - } - } + CertificateValidationStatus crlOutputStatus; + + CrlEntry crlEntry = crlService.findCrlEntryForCertificate(certificate.getSerialNumber().toString(16), crl.getUuid()); + if (crlEntry == null) { + crlOutputStatus = CertificateValidationStatus.VALID; + crlMessage.append("CRL verification successful from URL"); + crlMessage.append(". "); + } else { + crlOutputStatus = CertificateValidationStatus.REVOKED; + crlMessage.append("Certificate was revoked according to information from CRL URL"); + crlMessage.append(". Revocation reason: "); + crlMessage.append(crlEntry.getRevocationReason().getLabel()); + crlMessage.append(". "); + } return new CertificateValidationCheckDto(CertificateValidationCheck.CRL_VERIFICATION, crlOutputStatus, crlMessage.toString()); } + private CertificateValidationCheckDto checkBasicConstraints(X509Certificate certificate, X509Certificate issuerCertificate, boolean isEndCertificate) { int pathLenConstraint = certificate.getBasicConstraints(); boolean isCa = pathLenConstraint >= 0; @@ -383,4 +398,5 @@ private Map initializ validationOutput.put(CertificateValidationCheck.KEY_USAGE, new CertificateValidationCheckDto(CertificateValidationCheck.KEY_USAGE, CertificateValidationStatus.NOT_CHECKED, null)); return validationOutput; } + } diff --git a/src/main/resources/db/migration/V202311131230__trusted_ca_mark.sql b/src/main/resources/db/migration/V202311131230__trusted_ca_mark.sql new file mode 100644 index 000000000..52d97864c --- /dev/null +++ b/src/main/resources/db/migration/V202311131230__trusted_ca_mark.sql @@ -0,0 +1,2 @@ +ALTER TABLE certificate ADD COLUMN trusted_ca BOOLEAN NULL; +UPDATE certificate SET trusted_ca = false WHERE basic_constraints LIKE '%Subject Type=CA%'; diff --git a/src/main/resources/db/migration/V202312141140__authority_certifcat_uuid.sql b/src/main/resources/db/migration/V202312141140__authority_certifcat_uuid.sql new file mode 100644 index 000000000..19587ced6 --- /dev/null +++ b/src/main/resources/db/migration/V202312141140__authority_certifcat_uuid.sql @@ -0,0 +1 @@ +ALTER TABLE ra_profile ADD COLUMN authority_certificate_uuid UUID NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V202401171122__ca_crl_management.sql b/src/main/resources/db/migration/V202401171122__ca_crl_management.sql new file mode 100644 index 000000000..b3450df99 --- /dev/null +++ b/src/main/resources/db/migration/V202401171122__ca_crl_management.sql @@ -0,0 +1,24 @@ +CREATE TABLE crl ( + uuid UUID NOT NULL, + ca_certificate_uuid UUID, + issuer_dn VARCHAR NOT NULL, + serial_number VARCHAR NOT NULL, + crl_issuer_dn VARCHAR NOT NULL, + crl_number VARCHAR NOT NULL, + next_update TIMESTAMP NOT NULL, + crl_number_delta VARCHAR, + next_update_delta TIMESTAMP, + last_revocation_date TIMESTAMP, + PRIMARY KEY (uuid), + FOREIGN KEY (ca_certificate_uuid) REFERENCES certificate(uuid) +); + +CREATE TABLE crl_entry ( + crl_uuid UUID, + serial_number VARCHAR, + revocation_date TIMESTAMP NOT NULL, + revocation_reason VARCHAR NOT NULL, + PRIMARY KEY (crl_uuid, serial_number), + FOREIGN KEY (crl_uuid) REFERENCES crl(uuid) ON UPDATE CASCADE ON DELETE CASCADE +); + diff --git a/src/test/java/com/czertainly/core/service/CertificateServiceTest.java b/src/test/java/com/czertainly/core/service/CertificateServiceTest.java index 4f72f5d83..cc7893c74 100644 --- a/src/test/java/com/czertainly/core/service/CertificateServiceTest.java +++ b/src/test/java/com/czertainly/core/service/CertificateServiceTest.java @@ -9,9 +9,7 @@ import com.czertainly.api.model.common.attribute.v2.content.StringAttributeContent; import com.czertainly.api.model.common.attribute.v2.properties.MetadataAttributeProperties; import com.czertainly.api.model.core.auth.Resource; -import com.czertainly.api.model.core.certificate.CertificateDetailDto; -import com.czertainly.api.model.core.certificate.CertificateState; -import com.czertainly.api.model.core.certificate.CertificateValidationStatus; +import com.czertainly.api.model.core.certificate.*; import com.czertainly.api.model.core.connector.ConnectorStatus; import com.czertainly.api.model.core.connector.FunctionGroupCode; import com.czertainly.api.model.core.search.SearchFieldDataByGroupDto; @@ -353,4 +351,21 @@ public void testBulkRemove() throws NotFoundException { Assertions.assertThrows(NotFoundException.class, () -> certificateService.getCertificate(certificate.getSecuredUuid())); } + + @Test + public void testDownloadCertificate() throws NotFoundException, CertificateException, IOException { + CertificateContent certificateContentDownload = new CertificateContent(); + certificateContentDownload.setContent("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"); + certificateContentDownload = certificateContentRepository.save(certificateContentDownload); + certificate.setCertificateContent(certificateContentDownload); + testDownloadInternal(CertificateFormat.RAW, CertificateFormatEncoding.PEM); + testDownloadInternal(CertificateFormat.RAW, CertificateFormatEncoding.DER); + testDownloadInternal(CertificateFormat.PKCS7, CertificateFormatEncoding.PEM); + testDownloadInternal(CertificateFormat.PKCS7, CertificateFormatEncoding.DER); + } + + private void testDownloadInternal(CertificateFormat format, CertificateFormatEncoding encoding) throws NotFoundException, CertificateException, IOException { + CertificateDownloadResponseDto certificateDownloadResponseDto = certificateService.downloadCertificate(certificate.getUuid().toString(), format, encoding); + Assertions.assertDoesNotThrow(() -> (certificateService.createCertificate(certificateDownloadResponseDto.getContent(), CertificateType.X509))); + } } diff --git a/src/test/java/com/czertainly/core/service/CertificateValidationTest.java b/src/test/java/com/czertainly/core/service/CertificateValidationTest.java index 425fe9924..f5e5774c8 100644 --- a/src/test/java/com/czertainly/core/service/CertificateValidationTest.java +++ b/src/test/java/com/czertainly/core/service/CertificateValidationTest.java @@ -1,17 +1,34 @@ package com.czertainly.core.service; import com.czertainly.api.exception.NotFoundException; +import com.czertainly.api.exception.ValidationException; import com.czertainly.api.model.core.certificate.*; import com.czertainly.core.dao.entity.Certificate; import com.czertainly.core.dao.entity.CertificateContent; +import com.czertainly.core.dao.entity.Crl; import com.czertainly.core.dao.repository.CertificateContentRepository; import com.czertainly.core.dao.repository.CertificateRepository; +import com.czertainly.core.dao.repository.CrlEntryRepository; +import com.czertainly.core.dao.repository.CrlRepository; import com.czertainly.core.util.BaseSpringBootTest; +import com.czertainly.core.util.CertificateUtil; import com.czertainly.core.util.MetaDefinitions; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.CertIOException; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.util.Store; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; @@ -24,17 +41,22 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; +import java.math.BigInteger; +import java.security.*; +import java.security.cert.*; import java.util.*; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; + @SpringBootTest @Transactional @Rollback @@ -43,11 +65,20 @@ public class CertificateValidationTest extends BaseSpringBootTest { @Autowired private CertificateService certificateService; + @Autowired + private CrlService crlService; + @Autowired private CertificateRepository certificateRepository; @Autowired private CertificateContentRepository certificateContentRepository; + @Autowired + private CrlRepository crlRepository; + + @Autowired + private CrlEntryRepository crlEntryRepository; + private Certificate certificate; private CertificateContent certificateContent; @@ -61,8 +92,15 @@ public class CertificateValidationTest extends BaseSpringBootTest { private static final Logger logger = LoggerFactory.getLogger(CertificateValidationTest.class); + private WireMockServer mockServer; + + @BeforeEach public void setUp() throws GeneralSecurityException, IOException, com.czertainly.api.exception.CertificateException { + mockServer = new WireMockServer(0); + mockServer.start(); + + WireMock.configureFor("localhost", mockServer.port()); InputStream keyStoreStream = CertificateServiceTest.class.getClassLoader().getResourceAsStream("client1.p12"); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(keyStoreStream, "123456".toCharArray()); @@ -89,6 +127,8 @@ public void setUp() throws GeneralSecurityException, IOException, com.czertainly caCertificate = certificateService.createCertificate(caCertificateContent, CertificateType.X509); + certificateRepository.save(caCertificate); + String chainCertificateContent = "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"; chainIncompleteCertificate = certificateService.createCertificate(chainCertificateContent, CertificateType.X509); @@ -138,21 +178,123 @@ void testGetCertificateChain() throws NotFoundException { @Test void testDownloadCertificateChain() throws NotFoundException, CertificateException, CMSException, IOException { - CertificateChainDownloadResponseDto certificateChainIncompletePEMDownloadResponseDto = certificateService.downloadCertificateChain(chainIncompleteCertificate.getSecuredUuid(), CertificateFormat.PEM, true); + CertificateChainDownloadResponseDto certificateChainIncompletePEMDownloadResponseDto = certificateService.downloadCertificateChain(chainIncompleteCertificate.getSecuredUuid(), CertificateFormat.RAW, true, CertificateFormatEncoding.PEM); Assertions.assertEquals(2, getNumberOfCertificatesInPem(certificateChainIncompletePEMDownloadResponseDto.getContent())); Assertions.assertTrue(certificateChainIncompletePEMDownloadResponseDto.isCompleteChain()); - CertificateChainDownloadResponseDto certificateChainCompletePEMResponseDto = certificateService.downloadCertificateChain(chainCompleteCertificate.getSecuredUuid(), CertificateFormat.PEM, true); + CertificateChainDownloadResponseDto certificateChainCompletePEMResponseDto = certificateService.downloadCertificateChain(chainCompleteCertificate.getSecuredUuid(), CertificateFormat.RAW, true, CertificateFormatEncoding.PEM); Assertions.assertTrue(certificateChainCompletePEMResponseDto.isCompleteChain()); Assertions.assertEquals(2, getNumberOfCertificatesInPem(certificateChainCompletePEMResponseDto.getContent())); - CertificateChainDownloadResponseDto certificateChainCompletePKCS7ResponseDto = certificateService.downloadCertificateChain(chainCompleteCertificate.getSecuredUuid(), CertificateFormat.PKCS7, true); + CertificateChainDownloadResponseDto certificateChainCompletePKCS7ResponseDto = certificateService.downloadCertificateChain(chainCompleteCertificate.getSecuredUuid(), CertificateFormat.PKCS7, true, CertificateFormatEncoding.PEM); Assertions.assertTrue(certificateChainCompletePKCS7ResponseDto.isCompleteChain()); Assertions.assertEquals(2, getNumberOfCertificatesInPkcs7(certificateChainCompletePKCS7ResponseDto.getContent())); - CertificateChainDownloadResponseDto certificateChainResponseDto = certificateService.downloadCertificateChain(chainCompleteCertificate.getSecuredUuid(), CertificateFormat.PKCS7, false); + CertificateChainDownloadResponseDto certificateChainResponseDto = certificateService.downloadCertificateChain(chainCompleteCertificate.getSecuredUuid(), CertificateFormat.PKCS7, false, CertificateFormatEncoding.PEM); Assertions.assertTrue(certificateChainResponseDto.isCompleteChain()); Assertions.assertEquals(1, getNumberOfCertificatesInPkcs7(certificateChainResponseDto.getContent())); } + @Test + void testCrlProcessing() throws GeneralSecurityException, OperatorCreationException, IOException, NotFoundException { + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair pair = keyPairGen.generateKeyPair(); + + // prepare certs + X509Certificate x509CaCertificate = CertificateUtil.getX509Certificate(caCertificate.getCertificateContent().getContent()); + + List crlUrls = List.of(mockServer.baseUrl() + "/crl1.crl", mockServer.baseUrl() + "/crl2.crl"); + X509Certificate certificateWithCrl = createSelfSignedCertificateWithCrl("testCrl", crlUrls, null); + Certificate certificateWithCrlEntity = certificateService.createCertificateEntity(certificateWithCrl); + certificateWithCrlEntity.setTrustedCa(true); + certificateRepository.save(certificateWithCrlEntity); + + X509Certificate certificateWithDelta = createSelfSignedCertificateWithCrl("testCrlWithDelta", crlUrls, List.of(mockServer.baseUrl() + "/deltaCrl")); + Certificate certificateWithCrlDeltaEntity = certificateService.createCertificateEntity(certificateWithDelta); + certificateWithCrlDeltaEntity.setTrustedCa(true); + certificateRepository.save(certificateWithCrlDeltaEntity); + + // Test CRL without the end certificate and without delta + X509CRL emptyX509Crl = createEmptyCRL(x509CaCertificate, pair.getPrivate()); + byte[] emptyX509CrlBytes = emptyX509Crl.getEncoded(); + stubCrlPoint("/crl1.crl", emptyX509CrlBytes); + var validationResult = certificateService.getCertificateValidationResult(certificateWithCrlEntity.getSecuredUuid()); + Assertions.assertEquals(CertificateValidationStatus.VALID, validationResult.getValidationChecks().get(CertificateValidationCheck.CRL_VERIFICATION).getStatus()); + Crl crl = crlService.getCurrentCrl(certificateWithCrl, certificateWithCrl); + Assertions.assertNull(crlService.findCrlEntryForCertificate(certificateWithCrl.getSerialNumber().toString(16), crl.getUuid())); + + // Test CRL with revoked certificate and without delta and with one invalid CRL distribution point + crlRepository.delete(crl); + mockServer.removeStubMapping(mockServer.getStubMappings().get(0)); + X509CRL X509CrlRevokedCert = addRevocationToCRL(pair.getPrivate(), "SHA256WithRSAEncryption", emptyX509Crl, certificateWithCrl); + stubCrlPoint("/crl2.crl", X509CrlRevokedCert.getEncoded()); + validationResult = certificateService.getCertificateValidationResult(certificateWithCrlEntity.getSecuredUuid()); + Assertions.assertEquals(CertificateValidationStatus.REVOKED, validationResult.getValidationChecks().get(CertificateValidationCheck.CRL_VERIFICATION).getStatus()); + Crl crlWithRevoked = crlService.getCurrentCrl(certificateWithCrl, certificateWithCrl); + Assertions.assertNotNull(crlService.findCrlEntryForCertificate(certificateWithCrl.getSerialNumber().toString(16), crlWithRevoked.getUuid())); + + // Test properly set deltaCrl + X509CRL deltaCrl = createEmptyDeltaCRL(x509CaCertificate, pair.getPrivate(), BigInteger.valueOf(Integer.parseInt(crlWithRevoked.getCrlNumber())), BigInteger.ONE); + stubCrlPoint("/deltaCrl", deltaCrl.getEncoded()); + Crl crlWithDelta = crlService.getCurrentCrl(certificateWithDelta, certificateWithDelta); + Assertions.assertNotNull(crlWithDelta.getCrlNumberDelta()); + Assertions.assertNotNull(crlWithDelta.getNextUpdateDelta()); + + // Test improperly set deltaCrl + crlRepository.delete(crlWithDelta); + X509CRL deltaCrl2 = createEmptyDeltaCRL(x509CaCertificate, pair.getPrivate(), BigInteger.TWO, BigInteger.ONE); + stubCrlPoint("/deltaCrl", deltaCrl2.getEncoded()); + validationResult = certificateService.getCertificateValidationResult(certificateWithCrlDeltaEntity.getSecuredUuid()); + Assertions.assertEquals(CertificateValidationStatus.FAILED, validationResult.getValidationChecks().get(CertificateValidationCheck.CRL_VERIFICATION).getStatus()); + Assertions.assertThrows(ValidationException.class, () -> crlService.getCurrentCrl(certificateWithDelta, certificateWithDelta)); + + // Test deltaCrl with revoked cert + crlRepository.deleteAll(); + X509CRL deltaCrl3 = createEmptyDeltaCRL(x509CaCertificate, pair.getPrivate(), BigInteger.valueOf(Integer.parseInt(crlWithRevoked.getCrlNumber())), BigInteger.TWO); + X509CRL deltaCrlWithRevoked = addRevocationToCRL(pair.getPrivate(), "SHA256WithRSAEncryption", deltaCrl3, certificateWithDelta); + stubCrlPoint("/crl1.crl", emptyX509CrlBytes); + stubCrlPoint("/deltaCrl", deltaCrlWithRevoked.getEncoded()); + validationResult = certificateService.getCertificateValidationResult(certificateWithCrlDeltaEntity.getSecuredUuid()); + Assertions.assertEquals(CertificateValidationStatus.REVOKED, validationResult.getValidationChecks().get(CertificateValidationCheck.CRL_VERIFICATION).getStatus()); + Crl crlWithDelta2 = crlService.getCurrentCrl(certificateWithDelta, certificateWithDelta); + Assertions.assertNotNull(crlService.findCrlEntryForCertificate(certificateWithDelta.getSerialNumber().toString(16), crlWithDelta2.getUuid())); + } + + private void stubCrlPoint(String urlPart, byte[] body) { + mockServer.stubFor(WireMock + .get(WireMock.urlPathMatching(urlPart)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withHeader("Set-Cookie", "session_id=91837492837") + .withHeader("Set-Cookie", "split_test_group=B") + .withHeader("Cache-Control", "no-cache") + .withBody(body))); + } + + private X509Certificate createSelfSignedCertificateWithCrl(String commonName, List crlUrls, List deltaCrlUrls) throws CertIOException, NoSuchAlgorithmException, OperatorCreationException, CertificateException { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair pair = keyPairGen.generateKeyPair(); + SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(pair.getPublic().getEncoded()); + + X500Name x500Name = new X500Name("CN=" + commonName); + X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(x500Name, BigInteger.ONE, new Date(0), new Date(Long.MAX_VALUE), x500Name, subjectPublicKeyInfo); + CRLDistPoint crlDistPoint = new CRLDistPoint(createCrlDistributionPoints(crlUrls)); + certificateBuilder.addExtension(Extension.cRLDistributionPoints, false, crlDistPoint); + if (deltaCrlUrls != null) { + CRLDistPoint deltaCrlDistPoint = new CRLDistPoint(createCrlDistributionPoints(deltaCrlUrls)); + certificateBuilder.addExtension(Extension.freshestCRL, false, deltaCrlDistPoint); + } + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .build(pair.getPrivate()); + X509CertificateHolder holder = certificateBuilder.build(signer); + JcaX509CertificateConverter converter = new JcaX509CertificateConverter(); + converter.setProvider(new BouncyCastleProvider()); + return converter.getCertificate(holder); + } + + private int getNumberOfCertificatesInPkcs7(String content) throws CMSException { String pkcs7Data = new String(Base64.getDecoder().decode(content)); pkcs7Data = pkcs7Data.replace("-----BEGIN PKCS7-----", "").replace("-----END PKCS7-----", "").replaceAll("\\s", ""); @@ -175,5 +317,64 @@ private int getNumberOfCertificatesInPem(String content) throws IOException { return pemObjects.size(); } + public static Date calculateDate(int hoursInFuture) { + long secs = System.currentTimeMillis() / 1000; + return new Date((secs + ((long) hoursInFuture * 60 * 60)) * 1000); + } + + private X509CRL createEmptyCRL(X509Certificate caCert, PrivateKey caKey) throws CRLException, OperatorCreationException, CertIOException { + X509v2CRLBuilder crlGen = new X509v2CRLBuilder(X500Name.getInstance(caCert.getSubjectX500Principal().getEncoded()), calculateDate(0)); + crlGen.setNextUpdate(calculateDate(24 * 7)); + crlGen.addExtension(Extension.cRLNumber, + false, + new CRLNumber(BigInteger.ONE)); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .setProvider("BC").build(caKey); + + JcaX509CRLConverter converter = new JcaX509CRLConverter().setProvider("BC"); + return converter.getCRL(crlGen.build(signer)); + } + + private X509CRL createEmptyDeltaCRL(X509Certificate caCert, PrivateKey caKey, BigInteger deltaCrlIndicator, BigInteger deltaCrlNumber) throws CRLException, OperatorCreationException, CertIOException { + X509v2CRLBuilder crlGen = new X509v2CRLBuilder(X500Name.getInstance(caCert.getSubjectX500Principal().getEncoded()), calculateDate(0)); + crlGen.setNextUpdate(calculateDate(24 * 7)); + crlGen.addExtension(Extension.cRLNumber, false, new CRLNumber(deltaCrlNumber)); + crlGen.addExtension(Extension.deltaCRLIndicator, false, new CRLNumber(deltaCrlIndicator)); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .setProvider("BC").build(caKey); + + JcaX509CRLConverter converter = new JcaX509CRLConverter().setProvider("BC"); + return converter.getCRL(crlGen.build(signer)); + } + + public X509CRL addRevocationToCRL(PrivateKey caKey, String sigAlg, X509CRL crl, X509Certificate certToRevoke) throws IOException, GeneralSecurityException, OperatorCreationException { + JcaX509v2CRLBuilder crlGen = new JcaX509v2CRLBuilder(crl); + crlGen.setNextUpdate(calculateDate(24 * 7)); + + // add revocation + ExtensionsGenerator extGen = new ExtensionsGenerator(); + extGen.addExtension(Extension.reasonCode, false, org.bouncycastle.asn1.x509.CRLReason.lookup(2)); + crlGen.addCRLEntry(certToRevoke.getSerialNumber(), + new Date(), extGen.generate()); + ContentSigner signer = new JcaContentSignerBuilder(sigAlg) + .setProvider("BC").build(caKey); + JcaX509CRLConverter converter = new JcaX509CRLConverter().setProvider("BC"); + + return converter.getCRL(crlGen.build(signer)); + } + + private DistributionPoint[] createCrlDistributionPoints(List urls) { + List list = new ArrayList<>(); + for (String url : urls) { + DERIA5String deria5String = new DERIA5String(url); + GeneralName generalName = new GeneralName(GeneralName.uniformResourceIdentifier, deria5String); + DistributionPointName distributionPointName = new DistributionPointName(new GeneralNames(generalName)); + list.add(new DistributionPoint(distributionPointName, null, null)); + } + return list.toArray(new DistributionPoint[0]); + } + } diff --git a/src/test/java/com/czertainly/core/service/ClientOperationServiceV2Test.java b/src/test/java/com/czertainly/core/service/ClientOperationServiceV2Test.java index a50cb8381..57c57c00d 100644 --- a/src/test/java/com/czertainly/core/service/ClientOperationServiceV2Test.java +++ b/src/test/java/com/czertainly/core/service/ClientOperationServiceV2Test.java @@ -255,6 +255,7 @@ public void testRevokeCertificate() throws ConnectorException, CertificateExcept .willReturn(WireMock.okJson("true"))); ClientCertificateRevocationDto request = new ClientCertificateRevocationDto(); + request.setAttributes(List.of()); clientOperationService.revokeCertificateAction(certificate.getUuid(), request, true); } diff --git a/src/test/java/com/czertainly/core/service/RaProfileServiceTest.java b/src/test/java/com/czertainly/core/service/RaProfileServiceTest.java index 2eb66f42e..531b22edb 100644 --- a/src/test/java/com/czertainly/core/service/RaProfileServiceTest.java +++ b/src/test/java/com/czertainly/core/service/RaProfileServiceTest.java @@ -6,17 +6,14 @@ import com.czertainly.api.exception.ValidationException; import com.czertainly.api.model.client.approvalprofile.ApprovalProfileDto; import com.czertainly.api.model.client.approvalprofile.ApprovalProfileRelationDto; -import com.czertainly.api.model.client.approvalprofile.ApprovalProfileResponseDto; import com.czertainly.api.model.client.raprofile.AddRaProfileRequestDto; import com.czertainly.api.model.client.raprofile.EditRaProfileRequestDto; import com.czertainly.api.model.common.NameAndUuidDto; import com.czertainly.api.model.core.auth.Resource; import com.czertainly.api.model.core.connector.ConnectorStatus; import com.czertainly.api.model.core.raprofile.RaProfileDto; -import com.czertainly.api.model.core.scheduler.PaginationRequestDto; import com.czertainly.core.dao.entity.*; import com.czertainly.core.dao.repository.*; -import com.czertainly.core.security.authz.SecuredParentUUID; import com.czertainly.core.security.authz.SecuredUUID; import com.czertainly.core.security.authz.SecurityFilter; import com.github.tomakehurst.wiremock.WireMockServer; @@ -131,6 +128,13 @@ public void testAddRaProfile() throws ConnectorException, AlreadyExistException mockServer.stubFor(WireMock .get(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/raProfile/attributes")) .willReturn(WireMock.okJson("[]"))); + mockServer.stubFor(WireMock + .post(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/caCertificates")) + .willReturn(WireMock.okJson(""" + { + "certificates": [ + ] + }"""))); mockServer.stubFor(WireMock .post(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/raProfile/attributes/validate")) .willReturn(WireMock.okJson("true"))); @@ -280,4 +284,49 @@ public void testListOfApprovalProfilesByRAProfile() throws NotFoundException, Al Assertions.assertEquals(approvalProfile.getUuid().toString(), approvalProfileDto.getUuid()); } + @Test + public void testGetAuthorityCertificateChain() throws ConnectorException, AlreadyExistException { + mockServer.stubFor(WireMock + .post(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/caCertificates")) + .willReturn(WireMock.okJson(""" + { + "certificates": [ + { + "certificateData": "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", + "uuid": null, + "meta": null, + "certificateType": "X.509" + } ] + }"""))); + mockServer.stubFor(WireMock + .get(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/raProfile/attributes")) + .willReturn(WireMock.okJson("[]"))); + mockServer.stubFor(WireMock + .post(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/raProfile/attributes/validate")) + .willReturn(WireMock.okJson("true"))); + + EditRaProfileRequestDto request = new EditRaProfileRequestDto(); + request.setDescription("some description"); + request.setAttributes(List.of()); + raProfileService.editRaProfile(authorityInstanceReference.getSecuredParentUuid(), raProfile.getSecuredUuid(), request); + Assertions.assertNotNull(raProfile.getAuthorityCertificateUuid()); + + mockServer.stubFor(WireMock + .post(WireMock.urlPathMatching("/v1/authorityProvider/authorities/[^/]+/caCertificates")) + .willReturn(WireMock.okJson(""" + { + "certificates": [ + ] + }"""))); + raProfileService.editRaProfile(authorityInstanceReference.getSecuredParentUuid(), raProfile.getSecuredUuid(), request); + Assertions.assertNull(raProfile.getAuthorityCertificateUuid()); + + AddRaProfileRequestDto requestAdd = new AddRaProfileRequestDto(); + requestAdd.setName("testRaProfile2"); + requestAdd.setAttributes(List.of()); + RaProfileDto dto = raProfileService.addRaProfile(authorityInstanceReference.getSecuredParentUuid(), requestAdd); + Assertions.assertEquals(raProfile.getAuthorityCertificateUuid(), raProfileRepository.findByUuid(UUID.fromString(dto.getUuid())).get().getAuthorityCertificateUuid()); + + } + }