diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..391b392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.DS_Store + +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +.metadata +.classpath +.project +.settings +.springBeans +target +Servers +logs +generated +MANIFEST.MF +test-output +.factorypath + +# Intelij +*.iml +.idea +out + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/README.md b/README.md index e5645fc..cc92aa1 100644 --- a/README.md +++ b/README.md @@ -1 +1,176 @@ -# ca-cmc \ No newline at end of file +# CMC API for Certification Authority integration +____ + +This is an implementation of a CMC API for the CA engine with a narrowed scope to provide the essential functions of the CA via an open restful API. + +This CMC implementation supports two classes of requests: + +- Requests from an authorized RA (issue, revoke and get certificate) +- Requests for additional administrative operations + +The main difference between these classes of requests is that the request from an authorized RA relies purely on standard CMC request and response data defined in RFC 5272 while requests for additional administrative operations makes use of custom request and response data objects defined for this implementation profile. + +## Response and request syntax +### Generic request and response syntax + +This implementation of CMC is based on full PKI requests and full PKI responses only. All requests and responses have the form of CMS signatures carrying a CMC request or response data according to RFC 5272. + +The PKIData structure of requests only contains a controlSequence for control attributes and optionally a reqSequence for any certificate requests. + +The PKIResponse structure of responses only contains a controlSequence + +the cmcSequence and the otherMsgSequence in both requests and responses are always empty. + +The following control attributes are used in requests: + +Control attribute | Usage | Presence +---|---|--- +senderNonce | A unique byte array value | SHALL be present in all requests +regInfo | Carry implementation specific data for each request type | Conditional on request type +getCert | Used only in requests to get a specific certificate | SHALL be present in getCert requests and SHALL NOT be present in any other request +lraPOPWitness | Used with CRMF certificate requests | SHALL be present when CRMF certificate request format is used +revokeRequest | Used only in requests to revoke a certificate | SHALL be present in revoke requests and SHALL NOT be present in any other request + + +The following control attributes are used in responses: + +Control attribute | Usage | Presence +---|---|--- +recipientNonce | The nonce value of the corresponding request | SHALL be present in all responses +responseInfo | Carry implementation specific data for each request type | Conditional on request type +statusInfoV2 | Status information of the response | SHALL be present in all responses + +All control attributes above are defined in RFC 5272. + + +### Requests from authorized RA + +Standard requests from an authorized RA is limited to the following functions: + +- Certificate requests +- Certificate revocation +- Obtaining a specific certificate from the CA database + +The reason for this set of functions is that the scope of this implementation is limited to a specific use case where the RA acts as the exclusive entity that is authorized to request or revoke certificates on behalf of certificate subjects. The certificate subject has no direct contact with the CA in this scenario. As such we eliminate all need for a state machine and callback scenarios with multiple entities where for example one entity requests the certificate and another entity approves the certificate or similar. This reduces the protocol to a simple request/response protocol where the result is delivered directly as a response to any supported request. + +#### Certificate requests + +Certificate requests makes use of one of the following request formats: + +- PKCS#10 +- CRMF + +The PKCS#10 format MAY be used when the private key of the certificate subject is available to sign the certificate request as part of a POP process (Proof Of Possession). This can be achieved in several ways, for example where the actual subject provides a signed PKCS#10 to the RA or where the RA generates the key on behalf of the subject and therefore has access to the key to sign the PKCS#10 request. + +In all other cases where the RA can't provide a PKCS#10 request signed by the certificate subject's private key, the RA must assert to the certificate subjects POP of the private key by other means and assert this through the lraPOPWitness control attribute. In these cases, the PKCS#10 request format can't be used and therefore the Certificate Request Message Format (CRMF) must be used. + +Responses to a successful certificate request is provided in a signed PKIResponse object. The certificate that was issued, if any, is provided among the CMS certificates as defined in RFC 5272. + +#### Revocation requests + +Revocation requests make use of the revokeRequest control attribute, specifying the issuer name, certificate serial number of the certificate to revoke, reason code and revocation date. + +Successful revocation status is delivered in the response using the statusInfoV2 control attribute. + +#### Get cert requests + +The GetCert request makes use of the getCert control attribute to specify the issuer and the certificate serial number of the certificate to return. Returned certificate is included in the response exactly in the same way as when certificates are issued. i.e., in the set of certificates in the CMS SingedData structure. + +### Request for administrative operations + +All requests for administrative operations make use of the regInfo control attribute to hold custom request data and uses the responseInfo control attribute to return data in responses related to these custom requests. + +The following administrative operations are currently supported: + +- Get information about the target CA +- List all certificate serial numbers +- Get Certificates from the CA repository + +All admin service requests provide request data as a JSON string. This JSON string is a JSON serialization of the class se.swedenconnect.ca.cmc.model.admin.AdminCMCData + +AdminCMCData holds two data parameters: + +``` +/** Type of admin request */ +private AdminRequestType adminRequestType; +/** Admin request/response data */ +private String data; + +``` + +The data string holds another JSON string that contains the JSON serialization of the data object associated with the specified AdminRequestType declaration. + +The following table illustrate what data objects that are passed as request and response database + +AdminRequestType | Request data | Response data +---|---|--- +caInfo | absent | se.swedenconnect.ca.cmc.model.admin.response.CAInformation +listCerts | se.swedenconnect.ca.cmc.model.admin.request.ListCerts | List<se.swedenconnect.ca.cmc.model.admin.response.CertificateData> +allCertSerials | absent | List<String> (Serialnumbers as hex strings) + + +## CMC API + +This CMC API integration library provides java classes that can be used by both the client and the CA to implement this API. + +### CA integration + +A API for the CA is implemented using the interface class se.swedenconnect.ca.cmc.api.CMCCaApi + +This interface is implemented by the AbstractCMCCaApi and by the AbstractAdminCMCCaApi classes where AbstractCMCCaApi holds basic functions that should be valid for any implementation and where AbstractCMCCaApi adds a typical implementation of all custom admin functions. Finally there is a complete default implementation. The class DefaultCMCCaApi. The complete implementation must also provide functions for generating the final certificate content of any issued certificates. + +The CMCCaApi has one function: +> CMCResponse processRequest (CMCRequest cmcRequest) + +This function takes a CMCRequest as input and feeds that into the CA service and then returns the result of that operation in the form of a CMCResponse. + + +The DefaultCMCCaApi can be instantiated as follows: + +``` +CAService ca = getCAService(); //provide an implementation of the CA service interface +ContentSigner contentSigner = getContentSigner(); // Provide a CMS Content signer +List cmsSignerCerts = getResponseSignerCerts(); // Provide the CMS signer certificates +X509Certificate trustedClientCert = getTrustedClientCert(); // Provide a trusted client CMS signer certificate + +CMCResponseFactory cmcResponseFactory = new CMCResponseFactory(cmsSignerCerts, contentSigner); +CMCRequestParser cmcRequestParser = new CMCRequestParser(new DefaultCMCValidator(trustedClientCert), + new DefaultCMCReplayChecker()); +CMCCaApi cmcCaApi = new DefaultCMCCaApi(ca, cmcRequestParser, cmcResponseFactory); +``` + +All requests to the CA is then handled by processing incoming CMC requests and returning the resulting CMC response by executing the function as follows: + +CMCResponse response = cmcCaApi.processRequest(cmcRequest) + +### Client integration + +The client which may be an RA or a CA admin service, implements functioins to generate CMC Requests and to parse the CMCResponses returned from the CA. + +CMCReqeusts are created by an object of the se.swedenconnect.ca.cmc.api.CMCReqeustFactory class which may be instantiated as a Bean in a Spring application. + +CMCResponses are parsed by an object of the se.swedenconnect.ca.cmc.api.CMCResponseParser class, which also can be instantiated as a Bean. + +The following illustrates typical instantiations of these classes: + +``` +ContentSigner contentSigner = getContentSigner(); // Provide a CMS Content signer for the client +List clientSignerCerts = getClientSignerCerts(); // Provide the CMS signer certificates +X509Certificate caSignerCert = getCaSignerCert(); // Provide the trusted CA signer certificate +PublicKey caPublicKey = getCAPublicKey(); // Provide the public key of the CA + +CMCRequestFactory cmcRequestFactory = new CMCRequestFactory(clientSignerCerts, contentSigner); +CMCResponseParser cmcResponseParser = new CMCResponseParser(new DefaultCMCValidator(caSignerCert, caPublicKey); + +``` + +A CMC request is then created by providing a CMCRequestModel as input to the cmcRequestFactory as follows: + +> CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + +Four different implementations of CMCRequestModel are provided: + +- CMCCertificateRequestModel +- CMCRevokeRequestModel +- CMCGetCertRequestModel +- CMCAdminRequestModel diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ae4d890 --- /dev/null +++ b/pom.xml @@ -0,0 +1,119 @@ + + + + + 4.0.0 + + se.swedenconnect.ca + cmc + 1.0.3-SNAPSHOT + + + UTF-8 + 1.8 + 11 + 1.0.4 + 1.7.31 + + + + + org.projectlombok + lombok + 1.18.16 + provided + + + se.swedenconnect.ca + ca-engine + ${ca.engine.version} + + + org.bouncycastle + bcpkix-jdk15on + 1.67 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.4 + + + + se.idsec.sigval.base + cert-validation + 1.0.2 + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + methods + 10 + + + + + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + + prepare-agent + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + CA engine - ${project.version} + CA engine - ${project.version} + 8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 10 + 10 + + + + + + + \ No newline at end of file diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCCaApi.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCCaApi.java new file mode 100644 index 0000000..bd0fac4 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCCaApi.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.api.data.CMCResponse; + +import java.io.IOException; +import java.security.cert.CertificateException; + +/** + * The main interface for the CMC API + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public interface CMCCaApi { + + /** + * Process a CMC Request in the context of a CA service. This function shall never throw an exception caused by + * errors in the request. Any such error condition is captured in an error response with appropriate error code + * and a suitable error message. + * + * @param cmcRequestBytes the CMC request providing a request for service + * @return a CMC response providing the status and result data as a result of the service request + */ + CMCResponse processRequest (byte[] cmcRequestBytes); + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCCaApiException.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCCaApiException.java new file mode 100644 index 0000000..3cf5936 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCCaApiException.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import lombok.Getter; +import org.bouncycastle.asn1.cmc.BodyPartID; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; + +import java.io.IOException; +import java.util.List; + +/** + * Exception used within the CMC CA API. + * + * This Exception provides information about the CMC failure code as well as a list of body part IDs of CMC objects that caused the failure + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCCaApiException extends IOException { + + /** List of BodyPartID of CMC objects that was processed when the failure occurred */ + @Getter private final List failingBodyPartIds; + /** CMC failure type */ + @Getter private final CMCFailType cmcFailType; + + public CMCCaApiException(String message, List failingBodyPartIds, CMCFailType cmcFailType) { + super(message); + this.failingBodyPartIds = failingBodyPartIds; + this.cmcFailType = cmcFailType; + } + + public CMCCaApiException(String message, Throwable cause, List failingBodyPartIds, + CMCFailType cmcFailType) { + super(message, cause); + this.failingBodyPartIds = failingBodyPartIds; + this.cmcFailType = cmcFailType; + } + + public CMCCaApiException(Throwable cause, List failingBodyPartIds, CMCFailType cmcFailType) { + super(cause); + this.failingBodyPartIds = failingBodyPartIds; + this.cmcFailType = cmcFailType; + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCCertificateModelBuilder.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCCertificateModelBuilder.java new file mode 100644 index 0000000..3c7ae03 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCCertificateModelBuilder.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.cert.X509CertificateHolder; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuanceException; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuerModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.ExtensionModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.AuthorityKeyIdentifierModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.SubjectKeyIdentifierModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.AbstractCertificateModelBuilder; +import se.swedenconnect.ca.engine.ca.models.cert.impl.DefaultCertificateModelBuilder; +import se.swedenconnect.ca.engine.configuration.CAAlgorithmRegistry; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.util.List; + +/** + * Default certificate model builder implementation + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class CMCCertificateModelBuilder extends AbstractCertificateModelBuilder { + + /** Subject public key */ + private final PublicKey publicKey; + /** Certificate of the issuer */ + private final X509CertificateHolder issuer; + /** Algorithm used by the CA to sign certificates. This is used to identify the hash algorithm used to hash key identifiers */ + private final String caAlgorithm; + + /** + * Private constructor + * + * @param publicKey subject public key + * @param issuer issuer certificate + * @param caAlgorithm certificate signing algorithm + */ + private CMCCertificateModelBuilder(PublicKey publicKey, X509CertificateHolder issuer, + String caAlgorithm) { + this.publicKey = publicKey; + this.issuer = issuer; + this.caAlgorithm = caAlgorithm; + } + + /** + * Creates an instance of this certificate model builder + * + * @param publicKey subject public key + * @param issuer issuer certificate + * @param caAlgorithm certificate signing algorithm + * @return certificate model builder + */ + public static CMCCertificateModelBuilder getInstance(PublicKey publicKey, X509CertificateHolder issuer, + String caAlgorithm) { + return new CMCCertificateModelBuilder(publicKey, issuer, caAlgorithm); + } + + @Override public CertificateModel build() throws CertificateIssuanceException { + try { + return CertificateModel.builder() + .publicKey(publicKey) + .subject(getSubject()) + .extensionModels(getExtensionModels()) + .build(); + } + catch (Exception ex) { + throw new CertificateIssuanceException("Failed to prepare certificate data", ex); + } + } + + @Override + protected void getKeyIdentifierExtensionsModels(List extm) throws IOException { + + //Authority key identifier + if (includeAki) { + AuthorityKeyIdentifierModel akiModel = null; + try { + byte[] kidVal = SubjectKeyIdentifier.getInstance(issuer.getExtension(Extension.subjectKeyIdentifier).getParsedValue()) + .getKeyIdentifier(); + if (kidVal != null && kidVal.length > 0) { + akiModel = new AuthorityKeyIdentifierModel(new AuthorityKeyIdentifier(kidVal)); + } + } + catch (Exception ignored) { + } + + if (akiModel == null) { + akiModel = new AuthorityKeyIdentifierModel(new AuthorityKeyIdentifier( + getSigAlgoMessageDigest(caAlgorithm).digest(issuer.getSubjectPublicKeyInfo().getEncoded()) + )); + } + extm.add(akiModel); + } + + // Subject key identifier + if (includeSki) { + extm.add(new SubjectKeyIdentifierModel( + getSigAlgoMessageDigest(caAlgorithm).digest(publicKey.getEncoded()) + )); + } + + } + + /** + * Returns an instance of {@link MessageDigest} specified by the certificate signature algorithm + * + * @return message digest instance + */ + private MessageDigest getSigAlgoMessageDigest(String algorithm) { + MessageDigest messageDigestInstance = null; + try { + messageDigestInstance = CAAlgorithmRegistry.getMessageDigestInstance(algorithm); + } + catch (NoSuchAlgorithmException e) { + log.error("Illegal configured signature algorithm prevents retrieval of signature algorithm digest algorithm", e); + } + return messageDigestInstance; + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCParsingException.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCParsingException.java new file mode 100644 index 0000000..a17b2ec --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCParsingException.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import lombok.Getter; + +import java.io.IOException; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCParsingException extends IOException { + + @Getter private final byte[] nonce; + + public CMCParsingException(byte[] nonce) { + this.nonce = nonce; + } + + public CMCParsingException(String message, byte[] nonce) { + super(message); + this.nonce = nonce; + } + + public CMCParsingException(String message, Throwable cause, byte[] nonce) { + super(message, cause); + this.nonce = nonce; + } + + public CMCParsingException(Throwable cause, byte[] nonce) { + super(cause); + this.nonce = nonce; + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCRequestFactory.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCRequestFactory.java new file mode 100644 index 0000000..f1411ed --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCRequestFactory.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.asn1.crmf.CertReqMsg; +import org.bouncycastle.asn1.x509.CRLReason; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.cert.crmf.CRMFException; +import org.bouncycastle.cert.crmf.CertificateRequestMessage; +import org.bouncycastle.cert.crmf.CertificateRequestMessageBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.request.CMCRequestModel; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.request.impl.CMCAdminRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCCertificateRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCGetCertRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCRevokeRequestModel; +import se.swedenconnect.ca.engine.ca.attribute.AttributeValueEncoder; +import se.swedenconnect.ca.engine.configuration.CAAlgorithmRegistry; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * This class is intended to be used as a bean for creating CMC requests + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCRequestFactory { + + /** Random source */ + private final static SecureRandom RNG = new SecureRandom(); + /** Signer certificate chain for signing CMC requests */ + private final List signerCertChain; + /** A CMS Content signer used to sign CMC requests */ + private final ContentSigner signer; + + /** + * Constructor + * @param signerCertChain signer certificate chain for signing CMC requests + * @param signer a CMS Content signer used to sign CMC requests + */ + public CMCRequestFactory(List signerCertChain, ContentSigner signer) { + this.signerCertChain = signerCertChain; + this.signer = signer; + } + + /** + * Create a CMC Request + * @param cmcRequestModel model holding the data necessary to create a CMC request + * @return CMC Request + * @throws IOException on failure to create a valid CMC request + */ + public CMCRequest getCMCRequest(CMCRequestModel cmcRequestModel) throws IOException { + CMCRequest.CMCRequestBuilder requestBuilder = CMCRequest.builder(); + CMCRequestType cmcRequestType = cmcRequestModel.getCmcRequestType(); + Date messageTime = new Date(); + requestBuilder + .cmcRequestType(cmcRequestType) + .nonce(cmcRequestModel.getNonce()); + PKIData pkiData = null; + try { + switch (cmcRequestType) { + case issueCert: + pkiData = createCertRequest((CMCCertificateRequestModel) cmcRequestModel, messageTime); + addCertRequestData(pkiData, requestBuilder); + break; + case revoke: + pkiData = new PKIData(getCertRevocationControlSequence((CMCRevokeRequestModel) cmcRequestModel), + new TaggedRequest[] {}, new TaggedContentInfo[] {}, new OtherMsg[] {}); + break; + case admin: + pkiData = createAdminRequest((CMCAdminRequestModel) cmcRequestModel); + break; + case getCert: + pkiData = createGetCertRequest((CMCGetCertRequestModel) cmcRequestModel, messageTime); + break; + } + requestBuilder + .pkiData(pkiData) + .cmcRequestBytes(CMCUtils.signEncapsulatedCMSContent( + CMCObjectIdentifiers.id_cct_PKIData, + pkiData, signerCertChain, signer)); + } + catch (Exception ex) { + throw new IOException("Error generating CMC request", ex); + } + return requestBuilder.build(); + } + + private PKIData createGetCertRequest(CMCGetCertRequestModel cmcRequestModel, Date messageTime) { + return new PKIData(getGetCertsControlSequence(cmcRequestModel, messageTime), new TaggedRequest[] {}, new TaggedContentInfo[] {}, new OtherMsg[] {}); + } + + private TaggedAttribute[] getGetCertsControlSequence(CMCGetCertRequestModel cmcRequestModel, Date messageTime) { + List taggedAttributeList = new ArrayList<>(); + addNonceControl(taggedAttributeList, cmcRequestModel.getNonce()); + addRegistrationInfoControl(taggedAttributeList, cmcRequestModel); + GeneralName gn = new GeneralName(cmcRequestModel.getIssuerName()); + GetCert getCert = new GetCert(gn, cmcRequestModel.getSerialNumber()); + taggedAttributeList.add(getControl(CMCObjectIdentifiers.id_cmc_getCert, getCert)); + return taggedAttributeList.toArray(new TaggedAttribute[0]); + } + + private PKIData createAdminRequest(CMCAdminRequestModel cmcRequestModel) { + return new PKIData(getAdminControlSequence(cmcRequestModel), new TaggedRequest[] {}, new TaggedContentInfo[] {}, new OtherMsg[] {}); + } + + private TaggedAttribute[] getAdminControlSequence(CMCAdminRequestModel cmcRequestModel) { + List taggedAttributeList = new ArrayList<>(); + addNonceControl(taggedAttributeList, cmcRequestModel.getNonce()); + addRegistrationInfoControl(taggedAttributeList, cmcRequestModel); + return taggedAttributeList.toArray(new TaggedAttribute[0]); + } + + private PKIData createCertRequest(CMCCertificateRequestModel cmcRequestModel, Date messageTime) + throws NoSuchAlgorithmException, OperatorCreationException, IOException, CRMFException { + + TaggedRequest taggedCertificateRequest; + BodyPartID certReqBodyPartId = getBodyPartId(); + TaggedAttribute[] controlSequence = getCertRequestControlSequence(cmcRequestModel, cmcRequestModel.getNonce(), certReqBodyPartId); + PrivateKey certReqPrivate = cmcRequestModel.getCertReqPrivate(); + if (certReqPrivate != null) { + ContentSigner p10Signer = new JcaContentSignerBuilder(CAAlgorithmRegistry.getSigAlgoName(cmcRequestModel.getP10Algorithm())) + .build(certReqPrivate); + CertificationRequest certificationRequest = CMCUtils.getCertificationRequest(cmcRequestModel.getCertificateModel(), p10Signer, + new AttributeValueEncoder()); + taggedCertificateRequest = new TaggedRequest(new TaggedCertificationRequest(certReqBodyPartId, certificationRequest)); + } + else { + CertificateRequestMessageBuilder crmfBuilder = CMCUtils.getCRMFRequestMessageBuilder(certReqBodyPartId, + cmcRequestModel.getCertificateModel(), new AttributeValueEncoder()); + extendCertTemplate(crmfBuilder, cmcRequestModel); + CertificateRequestMessage certificateRequestMessage = crmfBuilder.build(); + taggedCertificateRequest = new TaggedRequest(certificateRequestMessage.toASN1Structure()); + } + + return new PKIData(controlSequence, new TaggedRequest[] { taggedCertificateRequest }, new TaggedContentInfo[] {}, new OtherMsg[] {}); + } + + /** + * Extension point for manipulating and extending the CRMF certificate template + * @param crmfBuilder the CRMF builder holding default certificate template data + * @param cmcRequestModel CMC request model holding data about the CMC request to be built + */ + protected void extendCertTemplate(CertificateRequestMessageBuilder crmfBuilder, CMCCertificateRequestModel cmcRequestModel) { + // Extend crmf cert template based on cmcRequestModel + } + + private static BodyPartID getBodyPartId() { + return getBodyPartId(new BigInteger(31, RNG).add(BigInteger.ONE)); + } + + private static BodyPartID getBodyPartId(BigInteger bodyPartId) { + long id = Long.parseLong(bodyPartId.toString(10)); + return new BodyPartID(id); + } + + private TaggedAttribute[] getCertRevocationControlSequence(CMCRevokeRequestModel cmcRequestModel) { + List taggedAttributeList = new ArrayList<>(); + addNonceControl(taggedAttributeList, cmcRequestModel.getNonce()); + addRegistrationInfoControl(taggedAttributeList, cmcRequestModel); + RevokeRequest revokeRequest = new RevokeRequest( + cmcRequestModel.getIssuerName(), + new ASN1Integer(cmcRequestModel.getSerialNumber()), + CRLReason.lookup(cmcRequestModel.getReason()), + new ASN1GeneralizedTime(cmcRequestModel.getRevocationDate()), null, null + ); + taggedAttributeList.add(getControl(CMCObjectIdentifiers.id_cmc_revokeRequest, revokeRequest)); + return taggedAttributeList.toArray(new TaggedAttribute[0]); + } + + private TaggedAttribute[] getCertRequestControlSequence(CMCCertificateRequestModel cmcRequestModel, byte[] nonce, + BodyPartID certReqBodyPartId) { + List taggedAttributeList = new ArrayList<>(); + addNonceControl(taggedAttributeList, nonce); + addRegistrationInfoControl(taggedAttributeList, cmcRequestModel); + if (cmcRequestModel.isLraPopWitness()) { + ASN1EncodableVector lraPopWitSeq = new ASN1EncodableVector(); + lraPopWitSeq.add(getBodyPartId()); + lraPopWitSeq.add(new DERSequence(certReqBodyPartId)); + taggedAttributeList.add(getControl(CMCObjectIdentifiers.id_cmc_lraPOPWitness, new DERSequence(lraPopWitSeq))); + } + return taggedAttributeList.toArray(new TaggedAttribute[0]); + } + + private void addRegistrationInfoControl(List taggedAttributeList, CMCRequestModel cmcRequestModel) { + byte[] registrationInfo = cmcRequestModel.getRegistrationInfo(); + if (registrationInfo != null) { + taggedAttributeList.add(getControl(CMCObjectIdentifiers.id_cmc_regInfo, new DEROctetString(registrationInfo))); + } + } + + public static void addNonceControl(List taggedAttributeList, byte[] nonce) { + if (nonce != null) { + taggedAttributeList.add(getControl(CMCObjectIdentifiers.id_cmc_senderNonce, new DEROctetString(nonce))); + } + } + + public static TaggedAttribute getControl(ASN1ObjectIdentifier oid, ASN1Encodable... values) { + return getControl(oid, null, values); + } + + public static TaggedAttribute getControl(ASN1ObjectIdentifier oid, BodyPartID id, ASN1Encodable... values) { + if (id == null) { + id = getBodyPartId(); + } + ASN1Set valueSet = getSet(values); + return new TaggedAttribute(id, oid, valueSet); + } + + public static ASN1Set getSet(ASN1Encodable... content) { + ASN1EncodableVector valueSet = new ASN1EncodableVector(); + for (ASN1Encodable data : content) { + valueSet.add(data); + } + return new DERSet(valueSet); + } + + private void addCertRequestData(PKIData pkiData, CMCRequest.CMCRequestBuilder cmcRequestBuilder) { + if (pkiData == null || pkiData.getReqSequence() == null) { + return; + } + TaggedRequest[] reqSequence = pkiData.getReqSequence(); + for (TaggedRequest taggedRequest : reqSequence) { + ASN1Encodable taggedRequestValue = taggedRequest.getValue(); + if (taggedRequestValue instanceof TaggedCertificationRequest) { + TaggedCertificationRequest taggedCertReq = (TaggedCertificationRequest) taggedRequestValue; + ASN1Sequence taggedCertReqSeq = ASN1Sequence.getInstance(taggedCertReq.toASN1Primitive()); + BodyPartID certReqBodyPartId = BodyPartID.getInstance(taggedCertReqSeq.getObjectAt(0)); + CertificationRequest certificationRequest = CertificationRequest.getInstance(taggedCertReqSeq.getObjectAt(1)); + cmcRequestBuilder + .certificationRequest(certificationRequest) + .certReqBodyPartId(certReqBodyPartId); + return; + } + if (taggedRequestValue instanceof CertReqMsg) { + CertificateRequestMessage certificateRequestMessage = new CertificateRequestMessage((CertReqMsg) taggedRequestValue); + ASN1Integer certReqId = ((CertReqMsg) taggedRequestValue).getCertReq().getCertReqId(); + BodyPartID certReqBodyPartId = new BodyPartID(certReqId.longValueExact()); + cmcRequestBuilder + .certificateRequestMessage(certificateRequestMessage) + .certReqBodyPartId(certReqBodyPartId); + return; + } + } + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCRequestParser.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCRequestParser.java new file mode 100644 index 0000000..35bae30 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCRequestParser.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.asn1.crmf.CertReqMsg; +import org.bouncycastle.cert.crmf.CertificateRequestMessage; +import org.bouncycastle.cms.CMSSignedData; +import se.swedenconnect.ca.cmc.api.data.CMCControlObjectID; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.auth.CMCReplayChecker; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.auth.CMCValidationResult; +import se.swedenconnect.ca.cmc.auth.CMCValidator; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; + +/** + * Parser for CMC Request data + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class CMCRequestParser { + + /** A validator used to validate signatures on a CMC request as well as the authorization granted to the CMC signer to make this request */ + private final CMCValidator validator; + /** Replay checker used to verify that the CMC message is not a replay of an old request */ + private final CMCReplayChecker replayChecker; + + /** + * Constructor for the CMC data parser + * @param validator the validator used to validate the signature and authorization of the CMC signer to provide a CMC request + */ + public CMCRequestParser(CMCValidator validator, CMCReplayChecker cmcReplayChecker) { + this.validator = validator; + this.replayChecker = cmcReplayChecker; + } + + /** + * Parse CMC request + * @param cmcRequestBytes the bytes of a CMC request + * @return {@link CMCRequest} + * @throws IOException on error parsing the CMC request bytes + */ + public CMCRequest parseCMCrequest(byte[] cmcRequestBytes) throws IOException { + CMCRequest cmcRequest = new CMCRequest(); + cmcRequest.setCmcRequestBytes(cmcRequestBytes); + + CMCValidationResult cmcValidationResult = validator.validateCMC(cmcRequestBytes); + if (!CMCObjectIdentifiers.id_cct_PKIData.equals(cmcValidationResult.getContentType())) { + throw new IOException("Illegal CMS content type for CMC request"); + } + if (!cmcValidationResult.isValid()){ + // Validation failed attempt to get nonce for an error response; + byte[] nonce = null; + try { + CMSSignedData signedData = cmcValidationResult.getSignedData(); + PKIData pkiData = PKIData.getInstance(new ASN1InputStream((byte[]) signedData.getSignedContent().getContent()).readObject()); + nonce = (byte[]) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_senderNonce, pkiData).getValue(); + } catch (Exception ex){ + throw new IOException("Unable to retrieve nonce value"); + } + throw new CMCParsingException(cmcValidationResult.getErrorMessage(),nonce); + } + try { + CMSSignedData signedData = cmcValidationResult.getSignedData(); + PKIData pkiData = PKIData.getInstance(new ASN1InputStream((byte[]) signedData.getSignedContent().getContent()).readObject()); + replayChecker.validate(signedData); + cmcRequest.setPkiData(pkiData); + // Get certification request + TaggedRequest[] reqSequence = pkiData.getReqSequence(); + if (reqSequence.length > 0) { + TaggedRequest taggedRequest = reqSequence[0]; + ASN1Encodable taggedRequestValue = taggedRequest.getValue(); + boolean popCheckOK = false; + if (taggedRequestValue instanceof TaggedCertificationRequest) { + TaggedCertificationRequest taggedCertReq = (TaggedCertificationRequest) taggedRequestValue; + ASN1Sequence taggedCertReqSeq = ASN1Sequence.getInstance(taggedCertReq.toASN1Primitive()); + BodyPartID certReqBodyPartId = BodyPartID.getInstance(taggedCertReqSeq.getObjectAt(0)); + cmcRequest.setCertReqBodyPartId(certReqBodyPartId); + CertificationRequest certificationRequest = CertificationRequest.getInstance(taggedCertReqSeq.getObjectAt(1)); + cmcRequest.setCertificationRequest(certificationRequest); + popCheckOK = true; + } + if (taggedRequestValue instanceof CertReqMsg) { + CertificateRequestMessage certificateRequestMessage = new CertificateRequestMessage((CertReqMsg) taggedRequestValue); + cmcRequest.setCertificateRequestMessage(certificateRequestMessage); + ASN1Integer certReqId = ((CertReqMsg) taggedRequestValue).getCertReq().getCertReqId(); + BodyPartID certReqBodyPartId = new BodyPartID(certReqId.longValueExact()); + cmcRequest.setCertReqBodyPartId(certReqBodyPartId); + popCheckOK = isLraWitnessMatch(pkiData, certReqBodyPartId); + } + if (!popCheckOK){ + throw new IllegalArgumentException("POP check failed"); + } + } + setRequestType(cmcRequest); + byte[] nonce = (byte[]) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_senderNonce, pkiData).getValue(); + cmcRequest.setNonce(nonce); + } + catch (Exception ex) { + if (ex instanceof IOException){ + throw (IOException) ex; + } + log.debug("Error parsing PKI Data from CMC request: {}", ex.toString()); + throw new IOException("Error parsing PKI Data from CMC request", ex); + } + return cmcRequest; + } + + private boolean isLraWitnessMatch(PKIData pkiData, BodyPartID certReqBodyPartId) throws IOException { + LraPopWitness lraPopWitness = (LraPopWitness) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_lraPOPWitness, pkiData).getValue(); + if (lraPopWitness != null) { + BodyPartID[] bodyIds = lraPopWitness.getBodyIds(); + return Arrays.asList(bodyIds).contains(certReqBodyPartId); + } + return false; + } + + private void setRequestType(CMCRequest cmcRequest) throws IOException { + if (cmcRequest.getCertificationRequest() != null || cmcRequest.getCertificateRequestMessage() != null){ + cmcRequest.setCmcRequestType(CMCRequestType.issueCert); + return; + } + if (CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_revokeRequest, cmcRequest.getPkiData()).getValue() != null){ + cmcRequest.setCmcRequestType(CMCRequestType.revoke); + return; + } + if (CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_getCert, cmcRequest.getPkiData()).getValue() != null){ + cmcRequest.setCmcRequestType(CMCRequestType.getCert); + return; + } + Object regInfoObj = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_regInfo, cmcRequest.getPkiData()).getValue(); + if (regInfoObj instanceof AdminCMCData){ + cmcRequest.setCmcRequestType(CMCRequestType.admin); + return; + } + throw new IOException("Illegal request type"); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCResponseFactory.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCResponseFactory.java new file mode 100644 index 0000000..eb37ee1 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCResponseFactory.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.operator.ContentSigner; +import se.swedenconnect.ca.cmc.api.data.*; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.response.CMCResponseModel; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * This class is intended to be used as a bean for creating CMC responses + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCResponseFactory { + + /** Signer certificate chain for signing CMC requests */ + private final List signerCertChain; + /** A CMS Content signer used to sign CMC requests */ + private final ContentSigner signer; + + /** + * Constructor + * @param signerCertChain signer certificate chain for signing CMC requests + * @param signer a CMS Content signer used to sign CMC requests + */ + public CMCResponseFactory(List signerCertChain, ContentSigner signer) { + this.signerCertChain = signerCertChain; + this.signer = signer; + } + + /** + * Create a CMC response + * @param cmcResponseModel response model holding data necessary to create the CMC response + * @return {@link CMCResponse} + * @throws IOException on errors creating a CMC response + */ + public CMCResponse getCMCResponse(CMCResponseModel cmcResponseModel) throws IOException { + try { + PKIResponse pkiResponseData = getPKIResponseData(cmcResponseModel); + List cmsCertList = new ArrayList<>(signerCertChain); + List outputCerts = cmcResponseModel.getReturnCertificates(); + if (outputCerts != null) { + cmsCertList.addAll(outputCerts); + } else { + outputCerts = new ArrayList<>(); + } + + CMCResponse.CMCResponseBuilder responseBuilder = CMCResponse.builder() + .nonce(cmcResponseModel.getNonce()) + .pkiResponse(pkiResponseData) + .cmcResponseBytes(CMCUtils.signEncapsulatedCMSContent(CMCObjectIdentifiers.id_cct_PKIResponse, pkiResponseData, cmsCertList, signer)) + .returnCertificates(outputCerts) + .responseStatus(cmcResponseModel.getCmcResponseStatus()) + .cmcRequestType(cmcResponseModel.getCmcRequestType()); + + return responseBuilder.build(); + } catch (Exception ex) { + throw new IOException("Error creating CMC Response", ex); + } + } + + private PKIResponse getPKIResponseData(CMCResponseModel cmcResponseModel) { + + ASN1EncodableVector pkiResponseSeq = new ASN1EncodableVector(); + ASN1EncodableVector controlSeq = new ASN1EncodableVector(); + ASN1EncodableVector cmsSeq = new ASN1EncodableVector(); + ASN1EncodableVector otherMsgSeq = new ASN1EncodableVector(); + + List controlAttrList = getControlAttributes(cmcResponseModel); + for (TaggedAttribute contrAttr : controlAttrList) { + controlSeq.add(contrAttr.toASN1Primitive()); + } + pkiResponseSeq.add(new DERSequence(controlSeq)); + pkiResponseSeq.add(new DERSequence(cmsSeq)); + pkiResponseSeq.add(new DERSequence(otherMsgSeq)); + + return PKIResponse.getInstance(new DERSequence(pkiResponseSeq)); + } + + private List getControlAttributes(CMCResponseModel cmcResponseModel) { + + List taggedAttributeList = new ArrayList<>(); + addNonceControl(taggedAttributeList, cmcResponseModel.getNonce()); + // Add response status and fail info + addStatusControl(taggedAttributeList, cmcResponseModel); + + // Add response info data + final byte[] responseInfo = cmcResponseModel.getResponseInfo(); + if (responseInfo != null) { + taggedAttributeList.add(CMCRequestFactory.getControl(CMCObjectIdentifiers.id_cmc_responseInfo, new DEROctetString(responseInfo))); + } + return taggedAttributeList; + } + + public static void addNonceControl(List taggedAttributeList, byte[] nonce) { + if (nonce != null) { + taggedAttributeList.add(CMCRequestFactory.getControl(CMCObjectIdentifiers.id_cmc_recipientNonce, new DEROctetString(nonce))); + } + } + + + + private void addStatusControl(List taggedAttributeList, CMCResponseModel cmcResponseModel) { + CMCResponseStatus cmcResponseStatus = cmcResponseModel.getCmcResponseStatus(); + CMCStatusType cmcStatusType = cmcResponseStatus.getStatus(); + CMCFailType cmcFailType = cmcResponseStatus.getFailType(); + String message = cmcResponseStatus.getMessage(); + CMCStatusInfoV2Builder statusBuilder = new CMCStatusInfoV2Builder(cmcStatusType.getCmcStatus(), + cmcResponseStatus.getBodyPartIDList().toArray(new BodyPartID[0])); + if (!cmcStatusType.equals(CMCStatusType.success) && cmcFailType != null) { + statusBuilder.setOtherInfo(cmcFailType.getCmcFailInfo()); + } + if (message != null) { + statusBuilder.setStatusString(message); + } + taggedAttributeList.add(CMCRequestFactory.getControl(CMCObjectIdentifiers.id_cmc_statusInfoV2, statusBuilder.build())); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/CMCResponseParser.java b/src/main/java/se/swedenconnect/ca/cmc/api/CMCResponseParser.java new file mode 100644 index 0000000..6edaa1e --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/CMCResponseParser.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSSignedData; +import se.swedenconnect.ca.cmc.api.data.*; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.auth.CMCValidationResult; +import se.swedenconnect.ca.cmc.auth.CMCValidator; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.engine.utils.CAUtils; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * Parser of CMC response data + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class CMCResponseParser { + + /** A validator used to validate signatures on a CMC request as well as the authorization granted to the CMC signer to make this request */ + private final CMCValidator validator; + /** The public key of the CA used to verify which of the return certificates that actually are issued by the responding CA */ + private final PublicKey caPublicKey; + + /** + * Constructor + * @param validator validator for validating signature on the response and the authorization of the responder + * @param caPublicKey public key of the CA + */ + public CMCResponseParser(CMCValidator validator, PublicKey caPublicKey) { + this.validator = validator; + this.caPublicKey = caPublicKey; + } + + /** + * Parsing a CMC response + * @param cmcResponseBytes the bytes of a CMC response + * @param cmcRequestType the type of CMC request this response is related to + * @return {@link CMCResponse} + * @throws IOException on error parsing the CMC response bytes + */ + public CMCResponse parseCMCresponse(byte[] cmcResponseBytes, CMCRequestType cmcRequestType) throws IOException { + + CMCResponse.CMCResponseBuilder responseBuilder = CMCResponse.builder(); + responseBuilder + .cmcResponseBytes(cmcResponseBytes) + .cmcRequestType(cmcRequestType); + + boolean expectCertsOnSuccess; + switch (cmcRequestType) { + case issueCert: + case getCert: + expectCertsOnSuccess = true; + break; + default: + expectCertsOnSuccess = false; + } + + CMCValidationResult cmcValidationResult = validator.validateCMC(cmcResponseBytes); + if (!CMCObjectIdentifiers.id_cct_PKIResponse.equals(cmcValidationResult.getContentType())) { + throw new IOException("Illegal CMS content type for CMC request"); + } + if (!cmcValidationResult.isValid()) { + throw new IOException(cmcValidationResult.getErrorMessage(), cmcValidationResult.getException()); + } + + try { + CMSSignedData signedData = cmcValidationResult.getSignedData(); + PKIResponse pkiResponse = PKIResponse.getInstance( + new ASN1InputStream((byte[]) signedData.getSignedContent().getContent()).readObject()); + responseBuilder.pkiResponse(pkiResponse); + byte[] nonce = (byte[]) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_recipientNonce, pkiResponse).getValue(); + CMCStatusInfoV2 statusInfoV2 = (CMCStatusInfoV2) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_statusInfoV2, + pkiResponse).getValue(); + CMCResponseStatus responseStatus = getResponseStatus(statusInfoV2); + responseBuilder + .nonce(nonce) + .responseStatus(responseStatus); + if (responseStatus.getStatus().equals(CMCStatusType.success) && expectCertsOnSuccess) { + // Success response where return certificates are expected. Get return certificates + responseBuilder.returnCertificates(getResponseCertificates(signedData, cmcValidationResult)); + } + else { + // No response success or no certificates expected in response. Return empty return certificate list + responseBuilder.returnCertificates(new ArrayList<>()); + } + } + catch (Exception ex) { + log.debug("Error parsing PKIResponse Data from CMC response", ex.toString()); + throw new IOException("Error parsing PKIResponse Data from CMC response", ex); + } + return responseBuilder.build(); + } + + CMCResponseStatus getResponseStatus(CMCStatusInfoV2 statusInfoV2) { + CMCFailType cmcFailType = getCmcFailType(statusInfoV2); + CMCStatusType cmcStatus = CMCStatusType.getCMCStatusType(statusInfoV2.getcMCStatus()); + String statusString = statusInfoV2.getStatusString() != null + ? statusInfoV2.getStatusString().getString() + : null; + BodyPartID[] bodyList = statusInfoV2.getBodyList(); + CMCResponseStatus cmcResponseStatus = new CMCResponseStatus( + cmcStatus, cmcFailType, statusString, Arrays.asList(bodyList) + ); + return cmcResponseStatus; + } + + public static CMCFailType getCmcFailType(CMCStatusInfoV2 statusInfoV2) { + OtherStatusInfo otherStatusInfo = statusInfoV2.getOtherStatusInfo(); + if (otherStatusInfo != null && otherStatusInfo.isFailInfo()) { + CMCFailInfo cmcFailInfo = CMCFailInfo.getInstance(otherStatusInfo.toASN1Primitive()); + return CMCFailType.getCMCFailType(cmcFailInfo); + } + return null; + } + + /** + * The process here is a bit complicated since the return certificates are mixed with the CMC signing certificates which may be issued + * by the CMC CA. The algorithm is as follows: + *

+ * 1) List all certificates in the CMS signature + * 2) Remove all certs not issued by the CA + * 3) If more than one certificate remains, remove any trusted CMS signer certificate + * + * @param signedData + * @param cmcValidationResult + * @return + * @throws CertificateException + * @throws IOException + */ + private List getResponseCertificates(CMSSignedData signedData, CMCValidationResult cmcValidationResult) + throws CertificateException, IOException { + Collection certsInCMS = signedData.getCertificates().getMatches(null); + List certificateList = new ArrayList<>(); + for (X509CertificateHolder certificateHolder : certsInCMS) { + certificateList.add(CAUtils.getCert(certificateHolder)); + } + // Remove all certs not issued by the CA + List caIssuedCertificateList = new ArrayList<>(); + for (X509Certificate cmsCert : certificateList) { + try { + cmsCert.verify(caPublicKey); + caIssuedCertificateList.add(cmsCert); + } + catch (InvalidKeyException | SignatureException e) { + continue; + } + catch (Exception e) { + throw new IOException("Invalid return certificate in CMC response"); + } + } + + if (caIssuedCertificateList.size() < 2) { + return caIssuedCertificateList; + } + + // More than 1 remaining cert. Remove any trusted CMS signer certificate + List filteredCertificateList = new ArrayList<>(); + List cmsSignerCertificatePath = CAUtils.getCertList(cmcValidationResult.getSignerCertificatePath()); + for (X509Certificate caIssuedCert : caIssuedCertificateList) { + if (!cmsSignerCertificatePath.contains(caIssuedCert)) { + filteredCertificateList.add(caIssuedCert); + } + } + return filteredCertificateList; + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCClient.java b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCClient.java new file mode 100644 index 0000000..09a789e --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCClient.java @@ -0,0 +1,210 @@ +package se.swedenconnect.ca.cmc.api.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import se.swedenconnect.ca.cmc.api.CMCCertificateModelBuilder; +import se.swedenconnect.ca.cmc.api.CMCRequestFactory; +import se.swedenconnect.ca.cmc.api.CMCResponseParser; +import se.swedenconnect.ca.cmc.api.client.impl.CMCClientHttpConnectorImpl; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.api.data.CMCResponse; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.auth.impl.DefaultCMCValidator; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.AdminRequestType; +import se.swedenconnect.ca.cmc.model.admin.request.ListCerts; +import se.swedenconnect.ca.cmc.model.admin.response.CAInformation; +import se.swedenconnect.ca.cmc.model.request.impl.CMCAdminRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCCertificateRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCGetCertRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCRevokeRequestModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.repository.SortBy; +import se.swedenconnect.ca.engine.configuration.CAAlgorithmRegistry; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class CMCClient { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final CMCRequestFactory cmcRequestFactory; + private final CMCResponseParser cmcResponseParser; + private CAInformation cachedCAInformation; + private Date lastCAInfoRecache; + private final URL cmcRequestUrl; + + @Setter private int connectTimeout = 1000; + @Setter private int readTimeout = 5000; + @Setter private int timeSkew = 60000; + @Setter private int maxAge = 60000; + @Setter private int caInfoMaxAge = 600000; + @Setter private CMCClientHttpConnector cmcClientHttpConnector; + + public CMCClient(String cmcRequestUrl, PrivateKey cmcSigningKey, X509Certificate cmcSigningCert, String algorithm, + X509Certificate cmcResponseCert, X509Certificate caCertificate) + throws MalformedURLException, NoSuchAlgorithmException, OperatorCreationException, CertificateEncodingException { + this.cmcRequestUrl = new URL(cmcRequestUrl); + ContentSigner contentSigner = new JcaContentSignerBuilder(CAAlgorithmRegistry.getSigAlgoName(algorithm)).build(cmcSigningKey); + this.cmcRequestFactory = new CMCRequestFactory(List.of(cmcSigningCert), contentSigner); + this.cmcResponseParser = new CMCResponseParser(new DefaultCMCValidator(cmcResponseCert), caCertificate.getPublicKey()); + this.cmcClientHttpConnector = new CMCClientHttpConnectorImpl(); + } + + public CMCResponse caInfoRequest() throws IOException { + + final CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(new CMCAdminRequestModel(AdminCMCData.builder() + .adminRequestType(AdminRequestType.caInfo) + .build())); + + return getCMCResponse(cmcRequest); + } + + public CMCResponse allSerialsRequest() throws IOException { + final CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(new CMCAdminRequestModel(AdminCMCData.builder() + .adminRequestType(AdminRequestType.allCertSerials) + .build())); + + return getCMCResponse(cmcRequest); + } + + public CMCResponse certIssuerRequest(CertificateModel certificateModel) throws IOException { + final CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(new CMCCertificateRequestModel(certificateModel, "crmf")); + return getCMCResponse(cmcRequest); + } + + public CMCResponse getCertRequest(BigInteger serialNumber) throws IOException { + final CAInformation caInformation = getCAInformation(false); + X509CertificateHolder caIssuerCert = new X509CertificateHolder(caInformation.getCertificateChain().get(0)); + final CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(new CMCGetCertRequestModel(serialNumber, caIssuerCert.getSubject())); + return getCMCResponse(cmcRequest); + } + + public CMCResponse revokeCertificateRequest(BigInteger serialNumber, int reason, Date revocationDate) throws IOException { + final CAInformation caInformation = getCAInformation(false); + X509CertificateHolder caIssuerCert = new X509CertificateHolder(caInformation.getCertificateChain().get(0)); + final CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(new CMCRevokeRequestModel( + serialNumber, + reason, + revocationDate, + caIssuerCert.getSubject() + )); + return getCMCResponse(cmcRequest); + } + + public CMCResponse listCertificatesRequest(int pageSize, int pageIndex, SortBy sortBy, boolean notRevoked) throws IOException { + final CMCRequest cmcRequest = cmcRequestFactory.getCMCRequest(new CMCAdminRequestModel(AdminCMCData.builder() + .adminRequestType(AdminRequestType.listCerts) + .data(OBJECT_MAPPER.writeValueAsString(ListCerts.builder() + .pageSize(pageSize) + .pageIndex(pageIndex) + .sortBy(sortBy) + .notRevoked(notRevoked) + .build())) + .build())); + return getCMCResponse(cmcRequest); + } + + /** + * Return a certificate model builder prepared for creating certificate models for certificate requests to this CA service via CMC + * + * @param subjectPublicKey the public key of the subject + * @param subject subject name data + * @param includeCrlDPs true to include CRL distribution point URLs in the issued certificate + * @param includeOcspURL true to include OCSP URL (if present) in the issued certificate + * @return certificate model builder + * @throws IOException errors obtaining the certificate model builder + */ + public CMCCertificateModelBuilder getCertificateModelBuilder(PublicKey subjectPublicKey, CertNameModel subject, + boolean includeCrlDPs, boolean includeOcspURL) throws IOException { + final CAInformation caInformation = getCAInformation(false); + X509CertificateHolder caIssuerCert = new X509CertificateHolder(caInformation.getCertificateChain().get(0)); + CMCCertificateModelBuilder certModelBuilder = CMCCertificateModelBuilder.getInstance(subjectPublicKey, caIssuerCert, + caInformation.getCaAlgorithm()); + + if (includeCrlDPs) { + certModelBuilder.crlDistributionPoints(caInformation.getCrlDpURLs()); + } + if (includeOcspURL) { + certModelBuilder.ocspServiceUrl(caInformation.getOcspResponserUrl()); + } + certModelBuilder.subject(subject); + return certModelBuilder; + } + + public CAInformation getCAInformation(boolean forceRecache) throws IOException { + if (!forceRecache) { + if (this.cachedCAInformation != null && lastCAInfoRecache != null) { + Date notBefore = new Date(System.currentTimeMillis() - caInfoMaxAge); + if (lastCAInfoRecache.after(notBefore)) { + // Re-cache is not forced and current cache is not too old. Use it. + return cachedCAInformation; + } + } + } + // Re-cache is required + cachedCAInformation = CMCResponseExtract.extractCAInformation(caInfoRequest()); + lastCAInfoRecache = new Date(); + return cachedCAInformation; + } + + private CMCResponse getCMCResponse(CMCRequest cmcRequest) throws IOException { + + CMCHttpResponseData httpResponseData = cmcClientHttpConnector.sendCmcRequest(cmcRequest.getCmcRequestBytes(), cmcRequestUrl, connectTimeout, readTimeout); + if (httpResponseData.getResponseCode() > 205 || httpResponseData.getException() != null){ + throw new IOException("Http connection to CA failed"); + } + byte[] cmcResponseBytes = httpResponseData.getData(); + Date notBefore = new Date(System.currentTimeMillis() - maxAge); + Date notAfter = new Date(System.currentTimeMillis() + timeSkew); + final Date signingTime; + try { + signingTime = CMCUtils.getSigningTime(cmcResponseBytes); + if (signingTime.before(notBefore)) { + throw new IOException("CMC Response is to old"); + } + if (signingTime.after(notAfter)) { + throw new IOException("CMC Response is predated - possible time skew problem"); + } + } + catch (CMSException e) { + throw new IOException("Error parsing signing time in CMC Response", e); + } + + CMCResponse cmcResponse = cmcResponseParser.parseCMCresponse(cmcResponseBytes, cmcRequest.getCmcRequestType()); + if (!Arrays.equals(cmcRequest.getNonce(), cmcResponse.getNonce())) { + throw new IOException("CMC response and request nonce mismatch"); + } + return cmcResponse; + + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCClientHttpConnector.java b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCClientHttpConnector.java new file mode 100644 index 0000000..44363d2 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCClientHttpConnector.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.client; + +import java.io.IOException; +import java.net.URL; + +/** + * interface for a connector that is responsible for sending and receiving data from the CA + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public interface CMCClientHttpConnector { + + /** + * Sending a request to a CA and getting a CMC response back or relevant error data + * @param cmcRequestBytes CMC request data + * @param requestUrl URL used to send the request + * @return response data + * @throws IOException + */ + CMCHttpResponseData sendCmcRequest(byte[] cmcRequestBytes, URL requestUrl, int connectTimeout, int readTimeout); + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCHttpResponseData.java b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCHttpResponseData.java new file mode 100644 index 0000000..6994cef --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCHttpResponseData.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.client; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Data class for the data returned as a result of a CMC request + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CMCHttpResponseData { + byte[] data; + int responseCode; + Exception exception; +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCResponseExtract.java b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCResponseExtract.java new file mode 100644 index 0000000..5199b92 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/client/CMCResponseExtract.java @@ -0,0 +1,55 @@ +package se.swedenconnect.ca.cmc.api.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; +import se.swedenconnect.ca.cmc.api.data.CMCControlObject; +import se.swedenconnect.ca.cmc.api.data.CMCResponse; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.AdminRequestType; +import se.swedenconnect.ca.cmc.model.admin.response.CAInformation; +import se.swedenconnect.ca.cmc.model.admin.response.CertificateData; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.io.IOException; +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCResponseExtract { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public static AdminCMCData getAdminCMCData (CMCResponse cmcResponse) throws IOException { + if (!cmcResponse.getCmcRequestType().equals(CMCRequestType.admin)){ + throw new IOException("Not an admin response"); + } + final CMCControlObject cmcControlObject = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_responseInfo, cmcResponse.getPkiResponse()); + AdminCMCData adminCMCData = (AdminCMCData) cmcControlObject.getValue(); + return adminCMCData; + } + + public static List extractCertificateData(CMCResponse cmcResponse) throws IOException { + final AdminCMCData adminCMCData = getAdminCMCData(cmcResponse); + if (!adminCMCData.getAdminRequestType().equals(AdminRequestType.listCerts)){ + throw new IOException("Not a list certificates response"); + } + List certificateDataList = OBJECT_MAPPER.readValue(adminCMCData.getData(), new TypeReference<>() {}); + return certificateDataList; + } + + public static CAInformation extractCAInformation(CMCResponse cmcResponse) throws IOException { + final AdminCMCData adminCMCData = getAdminCMCData(cmcResponse); + if (!adminCMCData.getAdminRequestType().equals(AdminRequestType.caInfo)){ + throw new IOException("Not a CA information response"); + } + CAInformation caInformation = OBJECT_MAPPER.readValue(adminCMCData.getData(), CAInformation.class); + return caInformation; + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/client/impl/CMCClientHttpConnectorImpl.java b/src/main/java/se/swedenconnect/ca/cmc/api/client/impl/CMCClientHttpConnectorImpl.java new file mode 100644 index 0000000..fa35649 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/client/impl/CMCClientHttpConnectorImpl.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.client.impl; + +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import se.swedenconnect.ca.cmc.api.client.CMCClientHttpConnector; +import se.swedenconnect.ca.cmc.api.client.CMCHttpResponseData; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +@NoArgsConstructor +public class CMCClientHttpConnectorImpl implements CMCClientHttpConnector { + + private static final String CMC_MIME_TYPE = "application/pkcs7-mime"; + + @Override + public CMCHttpResponseData sendCmcRequest(byte[] cmcRequestBytes, URL requestUrl, int connectTimeout, int readTimeout) { + try { + HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", CMC_MIME_TYPE); + connection.connect(); + try(OutputStream os = connection.getOutputStream()) { + os.write(cmcRequestBytes); + } + connection.setConnectTimeout(connectTimeout); + connection.setReadTimeout(readTimeout); + int responseCode = connection.getResponseCode(); + byte[] bytes; + try { + if (responseCode > 205) { + bytes = IOUtils.toByteArray(connection.getErrorStream()); + } else { + bytes = IOUtils.toByteArray(connection.getInputStream()); + } + } catch (IOException ex){ + log.debug("Error receiving http data stream {}", ex.toString()); + return CMCHttpResponseData.builder() + .data(null) + .exception(ex) + .responseCode(responseCode) + .build(); + } + return CMCHttpResponseData.builder() + .data(bytes) + .exception(null) + .responseCode(responseCode) + .build(); + } catch (Exception ex) { + log.debug("Error setting up HTTP connection {}", ex.toString()); + return CMCHttpResponseData.builder() + .data(null) + .exception(ex) + .responseCode(0) + .build(); + } + + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCControlObject.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCControlObject.java new file mode 100644 index 0000000..40cff69 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCControlObject.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bouncycastle.asn1.cmc.BodyPartID; + +/** + * Data class for a CMC control object + * + * These are also referred to in CMC as TaggedAttribute in CMC requests and responses + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CMCControlObject { + + /** Unique identifier of the control attribute */ + BodyPartID bodyPartID; + /** Object Identifier of the control attribute */ + CMCControlObjectID type; + /** Attribute value */ + Object value; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCControlObjectID.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCControlObjectID.java new file mode 100644 index 0000000..52614ad --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCControlObjectID.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Enumeration of CMC Control object identifiers + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Getter +@AllArgsConstructor +@Slf4j +public enum CMCControlObjectID { + statusInfo(CMCObjectIdentifiers.id_cmc_statusInfo), + identification(CMCObjectIdentifiers.id_cmc_identification), + identityProof(CMCObjectIdentifiers.id_cmc_identityProof), + dataReturn(CMCObjectIdentifiers.id_cmc_dataReturn), + transactionId(CMCObjectIdentifiers.id_cmc_transactionId), + senderNonce(CMCObjectIdentifiers.id_cmc_senderNonce), + recipientNonce(CMCObjectIdentifiers.id_cmc_recipientNonce), + addExtensions(CMCObjectIdentifiers.id_cmc_addExtensions), + encryptedPOP(CMCObjectIdentifiers.id_cmc_encryptedPOP), + decryptedPOP(CMCObjectIdentifiers.id_cmc_decryptedPOP), + lraPOPWitness(CMCObjectIdentifiers.id_cmc_lraPOPWitness), + getCert(CMCObjectIdentifiers.id_cmc_getCert), + getCRL(CMCObjectIdentifiers.id_cmc_getCRL), + revokeRequest(CMCObjectIdentifiers.id_cmc_revokeRequest), + regInfo(CMCObjectIdentifiers.id_cmc_regInfo), + responseInfo(CMCObjectIdentifiers.id_cmc_responseInfo), + queryPending(CMCObjectIdentifiers.id_cmc_queryPending), + popLinkRandom(CMCObjectIdentifiers.id_cmc_popLinkRandom), + popLinkWitness(CMCObjectIdentifiers.id_cmc_popLinkWitness), + popLinkWitnessV2(CMCObjectIdentifiers.id_cmc_popLinkWitnessV2), + confirmCertAcceptance(CMCObjectIdentifiers.id_cmc_confirmCertAcceptance), + statusInfoV2(CMCObjectIdentifiers.id_cmc_statusInfoV2), + trustedAnchors(CMCObjectIdentifiers.id_cmc_trustedAnchors), + authData(CMCObjectIdentifiers.id_cmc_authData), + batchRequests(CMCObjectIdentifiers.id_cmc_batchRequests), + batchResponses(CMCObjectIdentifiers.id_cmc_batchResponses), + publishCert(CMCObjectIdentifiers.id_cmc_publishCert), + modCertTemplate(CMCObjectIdentifiers.id_cmc_modCertTemplate), + controlProcessed(CMCObjectIdentifiers.id_cmc_controlProcessed), + identityProofV2(CMCObjectIdentifiers.id_cmc_identityProofV2); + + + private ASN1ObjectIdentifier oid; + + /** + * Return the Enum instance of the CMC Control object identifier matching a specified ASN OID + * @param oid ASN.1 OID + * @return Enum instance if match found, or else null + */ + public static CMCControlObjectID getControlObjectID(String oid){ + try { + return getControlObjectID(new ASN1ObjectIdentifier(oid)); + } catch (Exception ex){ + log.debug("Illegal Object Identifier: {}", ex.toString()); + return null; + } + } + + /** + * Return the Enum instance of the CMC Control object identifier matching a specified ASN OID + * @param oid ASN.1 OID + * @return Enum instance if match found, or else null + */ + public static CMCControlObjectID getControlObjectID(ASN1ObjectIdentifier oid){ + return Arrays.stream(values()) + .filter(cmcControlObjectID -> cmcControlObjectID.getOid().equals(oid)) + .findFirst() + .orElse(null); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCFailType.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCFailType.java new file mode 100644 index 0000000..e2ab481 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCFailType.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.cmc.CMCFailInfo; +import org.bouncycastle.asn1.cmc.CMCStatus; + +import java.util.Arrays; + +/** + * Enumeration of CMC fail types + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@AllArgsConstructor +@Getter +@Slf4j +public enum CMCFailType { + + badAlg(CMCFailInfo.badAlg, 0), + badMessageCheck(CMCFailInfo.badMessageCheck, 1), + badRequest(CMCFailInfo.badRequest, 2), + badTime(CMCFailInfo.badTime, 3), + badCertId(CMCFailInfo.badCertId, 4), + confirmRequired(CMCFailInfo.unsupportedExt, 5), + mustArchiveKeys(CMCFailInfo.mustArchiveKeys, 6), + partial(CMCFailInfo.badIdentity, 7), + popRequired(CMCFailInfo.popRequired, 8), + popFailed(CMCFailInfo.popFailed, 9), + noKeyReuse(CMCFailInfo.noKeyReuse, 10), + internalCAError(CMCFailInfo.internalCAError, 11), + tryLater(CMCFailInfo.tryLater, 12), + authDataFail(CMCFailInfo.authDataFail, 13); + + private CMCFailInfo cmcFailInfo; + private int value; + + /** + * Get CMCFailInfoType from integer value + * @param value the integer value of a CMC Fail Info according to RFC 5272 + * @return {@link CMCFailType} + */ + public static CMCFailType getCMCFailType(int value){ + return Arrays.stream(values()) + .filter(cmcStatusType -> cmcStatusType.getValue() == value) + .findFirst() + .orElse(null); + } + + /** + * Get CMCFailType from CMCFailInfo value + * @param cmcFailInfo CMCFailInfo value + * @return {@link CMCFailType} + */ + public static CMCFailType getCMCFailType(CMCFailInfo cmcFailInfo){ + try { + int intVal = ((ASN1Integer) cmcFailInfo.toASN1Primitive()).intPositiveValueExact(); + return getCMCFailType(intVal); + } catch (Exception ex) { + log.debug("Bad CMCFailInfo syntax", ex); + return null; + } + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCRequest.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCRequest.java new file mode 100644 index 0000000..c1de6b8 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCRequest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.*; +import org.bouncycastle.asn1.cmc.BodyPartID; +import org.bouncycastle.asn1.cmc.CertificationRequest; +import org.bouncycastle.asn1.cmc.PKIData; +import org.bouncycastle.cert.crmf.CertificateRequestMessage; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.util.Date; + +/** + * Data class for CMC request data + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CMCRequest { + + /** The bytes of the CMC request */ + private byte[] cmcRequestBytes; + /** The request nonce */ + private byte[] nonce; + /** The type of request according to local type declaration */ + private CMCRequestType cmcRequestType; + /** The PKCS#10 request in this CMC request, if present */ + private CertificationRequest certificationRequest; + /** The CRMF certificate request in this CMC request, if present */ + private CertificateRequestMessage certificateRequestMessage; + /** The BodyPartId (or CRMF ID) of the certificate request in this CMC request, if present */ + private BodyPartID certReqBodyPartId; + /** The PKIData structure of this CMC request */ + private PKIData pkiData; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCResponse.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCResponse.java new file mode 100644 index 0000000..60c9cf8 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.*; +import org.bouncycastle.asn1.cmc.PKIResponse; +import se.swedenconnect.ca.cmc.auth.CMCValidator; +import se.swedenconnect.ca.cmc.model.request.CMCRequestModel; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +/** + * Data class for CMC response data + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CMCResponse { + + /** The type of request this response is responding to */ + private CMCRequestType cmcRequestType; + /** the bytes of the CMC Response */ + private byte[] cmcResponseBytes; + /** the response nonce value */ + private byte[] nonce; + /** the certificates returned in the response except for the CMS signing certificates */ + private List returnCertificates; + /** The PKIResponse data of the response */ + private PKIResponse pkiResponse; + /** Response status of the response */ + private CMCResponseStatus responseStatus; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCResponseStatus.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCResponseStatus.java new file mode 100644 index 0000000..f325b27 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCResponseStatus.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bouncycastle.asn1.cmc.BodyPartID; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; +import se.swedenconnect.ca.cmc.api.data.CMCStatusType; + +import java.util.List; + +/** + * Data class for CMC response status information + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CMCResponseStatus { + + public CMCResponseStatus(CMCStatusType status, List bodyPartIDList) { + this.status = status; + this.bodyPartIDList = bodyPartIDList; + } + + /** The major status indicating success or failure */ + private CMCStatusType status; + /** Detailed failure information as provided by {@link CMCFailType} */ + private CMCFailType failType; + /** Status message, normally null on success responses */ + private String message; + /** List of request control message body part ID:s that was processed in the request to obtain the response */ + private List bodyPartIDList; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCStatusType.java b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCStatusType.java new file mode 100644 index 0000000..61468c8 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/data/CMCStatusType.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.cmc.CMCStatus; + +import java.util.Arrays; + +/** + * Enumeration of status type + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@AllArgsConstructor +@Getter +@Slf4j +public enum CMCStatusType { + + success(CMCStatus.success, 0), + failed(CMCStatus.failed, 2), + pending(CMCStatus.pending, 3), + noSupport(CMCStatus.noSupport, 4), + confirmRequired(CMCStatus.confirmRequired, 5), + popRequired(CMCStatus.popRequired, 6), + partial(CMCStatus.partial, 7); + + private CMCStatus cmcStatus; + private int value; + + /** + * Get CMCStatus from integer value + * @param value the integer value of a CMC Status according to RFC 5272 + * @return {@link CMCStatusType} + */ + public static CMCStatusType getCMCStatusType(int value){ + return Arrays.stream(values()) + .filter(cmcStatusType -> cmcStatusType.getValue() == value) + .findFirst() + .orElse(null); + } + + /** + * Get CMCStatus from CMCStatus value + * @param cmcStatus CMCStatus value + * @return {@link CMCStatusType} + */ + public static CMCStatusType getCMCStatusType(CMCStatus cmcStatus){ + try { + int intVal = ((ASN1Integer) cmcStatus.toASN1Primitive()).intPositiveValueExact(); + return getCMCStatusType(intVal); + } catch (Exception ex) { + log.debug("Bad CMCStatus syntax", ex); + return null; + } + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/impl/AbstractAdminCMCCaApi.java b/src/main/java/se/swedenconnect/ca/cmc/api/impl/AbstractAdminCMCCaApi.java new file mode 100644 index 0000000..8102c50 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/impl/AbstractAdminCMCCaApi.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import se.swedenconnect.ca.cmc.api.CMCRequestParser; +import se.swedenconnect.ca.cmc.api.CMCResponseFactory; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.AdminRequestType; +import se.swedenconnect.ca.cmc.model.admin.request.ListCerts; +import se.swedenconnect.ca.cmc.model.admin.response.CAInformation; +import se.swedenconnect.ca.cmc.model.admin.response.CertificateData; +import se.swedenconnect.ca.engine.ca.issuer.CAService; +import se.swedenconnect.ca.engine.ca.repository.CARepository; +import se.swedenconnect.ca.engine.ca.repository.CertificateRecord; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The default admin implementation of the CMC CA API + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public abstract class AbstractAdminCMCCaApi extends AbstractCMCCaApi { + public AbstractAdminCMCCaApi(CAService caService, + CMCRequestParser cmcRequestParser, CMCResponseFactory cmcResponseFactory) { + super(caService, cmcRequestParser, cmcResponseFactory); + } + + @Override protected AdminCMCData getAdminResponse(AdminCMCData adminRequest) throws Exception { + + AdminRequestType adminRequestType = adminRequest.getAdminRequestType(); + String responseInfo = null; + + switch (adminRequestType) { + case caInfo: + responseInfo = getCAinfoResponse(); + break; + case listCerts: + ListCerts listCertsReqeust = CMCUtils.OBJECT_MAPPER.readValue(adminRequest.getData(), ListCerts.class); + responseInfo = getListCertsResponse(listCertsReqeust); + break; + case allCertSerials: + responseInfo = getAllCertSerials(); + break; + } + + return AdminCMCData.builder() + .adminRequestType(adminRequestType) + .data(responseInfo) + .build(); + } + + private String getAllCertSerials() throws JsonProcessingException { + List allCertificates = caService.getCaRepository().getAllCertificates(); + List allCertSerialStrings = allCertificates.stream() + .map(bigInteger -> bigInteger.toString(16)) + .collect(Collectors.toList()); + return CMCUtils.OBJECT_MAPPER.writeValueAsString(allCertSerialStrings); + } + + private String getListCertsResponse(ListCerts listCertsReqeust) throws JsonProcessingException { + CARepository caRepository = caService.getCaRepository(); + List certificateRange = caRepository.getCertificateRange( + listCertsReqeust.getPageIndex(), + listCertsReqeust.getPageSize(), + listCertsReqeust.isNotRevoked(), + listCertsReqeust.getSortBy() + ); + + List certificateDataList = new ArrayList<>(); + for (CertificateRecord certificateRecord : certificateRange) { + CertificateData.CertificateDataBuilder builder = CertificateData.builder() + .certificate(certificateRecord.getCertificate()) + .revoked(certificateRecord.isRevoked()); + + if (certificateRecord.isRevoked()) { + builder + .revocationReason(certificateRecord.getReason()) + .revocationDate(certificateRecord.getRevocationTime().getTime()); + } + certificateDataList.add(builder.build()); + } + return CMCUtils.OBJECT_MAPPER.writeValueAsString(certificateDataList); + } + + private String getCAinfoResponse() throws Exception { + CARepository caRepository = caService.getCaRepository(); + CAInformation caInformation = CAInformation.builder() + .validCertificateCount(caRepository.getCertificateCount(true)) + .certificateCount(caRepository.getCertificateCount(false)) + .certificateChain(CMCUtils.getCerHolderByteList(caService.getCACertificateChain())) + .ocspCertificate(caService.getOCSPResponderCertificate() != null + ? caService.getOCSPResponderCertificate().getEncoded() + : null) + .caAlgorithm(caService.getCaAlgorithm()) + .ocspResponserUrl(caService.getOCSPResponderURL()) + .crlDpURLs(caService.getCrlDpURLs()) + .build(); + return CMCUtils.OBJECT_MAPPER.writeValueAsString(caInformation); + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/impl/AbstractCMCCaApi.java b/src/main/java/se/swedenconnect/ca/cmc/api/impl/AbstractCMCCaApi.java new file mode 100644 index 0000000..a2b9a33 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/impl/AbstractCMCCaApi.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.impl; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import se.swedenconnect.ca.cmc.api.*; +import se.swedenconnect.ca.cmc.api.data.*; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.response.CMCResponseModel; +import se.swedenconnect.ca.cmc.model.response.impl.CMCAdminResponseModel; +import se.swedenconnect.ca.cmc.model.response.impl.CMCBasicCMCResponseModel; +import se.swedenconnect.ca.engine.ca.issuer.CAService; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.repository.CertificateRecord; +import se.swedenconnect.ca.engine.utils.CAUtils; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Basic abstract CMC CA API implementation + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public abstract class AbstractCMCCaApi implements CMCCaApi { + + protected final CAService caService; + protected final CMCRequestParser cmcRequestParser; + protected final CMCResponseFactory cmcResponseFactory; + + public AbstractCMCCaApi(CAService caService, CMCRequestParser cmcRequestParser, + CMCResponseFactory cmcResponseFactory) { + this.caService = caService; + this.cmcRequestParser = cmcRequestParser; + this.cmcResponseFactory = cmcResponseFactory; + } + + @Override public CMCResponse processRequest(byte[] cmcRequestBytes) { + + byte[] nonce = new byte[]{}; + + try { + CMCRequest cmcRequest = cmcRequestParser.parseCMCrequest(cmcRequestBytes); + nonce = cmcRequest.getNonce(); + CMCRequestType cmcRequestType = cmcRequest.getCmcRequestType(); + switch (cmcRequestType) { + + case issueCert: + return processCertIssuingRequest(cmcRequest); + case revoke: + return processRevokeRequest(cmcRequest); + case admin: + return processCustomRequest(cmcRequest); + case getCert: + return processGetCertRequest(cmcRequest); + default: + throw new IllegalArgumentException("Unrecognized CMC request type"); + } + } + catch (Exception ex) { + try { + if (ex instanceof CMCParsingException) { + CMCParsingException cmcParsingException = (CMCParsingException) ex; + CMCResponseModel responseModel = new CMCBasicCMCResponseModel( + cmcParsingException.getNonce(), + CMCResponseStatus.builder() + .status(CMCStatusType.failed) + .failType(CMCFailType.badRequest) + .message(ex.getMessage()) + .bodyPartIDList(new ArrayList<>()) + .build(), + null, null + ); + return cmcResponseFactory.getCMCResponse(responseModel); + } + if (ex instanceof CMCCaApiException) { + // Processing CMC request resulted in a error exception. + CMCCaApiException cmcException = (CMCCaApiException) ex; + CMCResponseModel responseModel = new CMCBasicCMCResponseModel( + nonce, + CMCResponseStatus.builder() + .status(CMCStatusType.failed) + .failType(cmcException.getCmcFailType()) + .message(ex.getMessage()) + .bodyPartIDList(cmcException.getFailingBodyPartIds()) + .build(), + + null, null + ); + return cmcResponseFactory.getCMCResponse(responseModel); + } + else { + // Processing CMC request resulted in a general exception caused by internal CA error. + CMCResponseModel responseModel = new CMCBasicCMCResponseModel( + nonce, + CMCResponseStatus.builder() + .status(CMCStatusType.failed) + .failType(CMCFailType.internalCAError) + .message(ex.getMessage()) + .bodyPartIDList(new ArrayList<>()) + .build(), + + null, null + ); + return cmcResponseFactory.getCMCResponse(responseModel); + } + } + catch (Exception e) { + // This should never happen unless there is a serious bug or configuration error + // The exception caught here is related to parsing returnCertificates which is passed as a null parameter in this case + e.printStackTrace(); + log.error("Critical exception in CA API implementation", e); + throw new RuntimeException("Critical exception in CA API implementation", e); + } + } + } + + protected CMCResponse processCertIssuingRequest(CMCRequest cmcRequest) throws CMCCaApiException { + + try { + CertificateModel certificateModel = getCertificateModel(cmcRequest); + X509CertificateHolder certificateHolder = caService.issueCertificate(certificateModel); + + CMCResponseModel responseModel = new CMCBasicCMCResponseModel( + cmcRequest.getNonce(), + new CMCResponseStatus(CMCStatusType.success, Arrays.asList(cmcRequest.getCertReqBodyPartId())), + cmcRequest.getCmcRequestType(), + (byte[]) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_regInfo, cmcRequest.getPkiData()).getValue(), + Arrays.asList(certificateHolder) + ); + + return cmcResponseFactory.getCMCResponse(responseModel); + } + catch (Exception ex) { + List failingBodyPartIds = cmcRequest.getCertReqBodyPartId() == null + ? new ArrayList<>() + : Arrays.asList(cmcRequest.getCertReqBodyPartId()); + throw new CMCCaApiException(ex, failingBodyPartIds, CMCFailType.badRequest); + } + } + + /** + * This functions generates a certificate request model from the certificate request and control parameters from a CMC request + * + * @param cmcRequest CMC Request + * @return certificate model + * @throws Exception Any exception caught while attempting to create a certificate model from the CMC request + */ + abstract CertificateModel getCertificateModel(CMCRequest cmcRequest) throws Exception; + + protected CMCResponse processRevokeRequest(CMCRequest cmcRequest) throws CMCCaApiException { + try { + PKIData pkiData = cmcRequest.getPkiData(); + CMCControlObject cmcControlObject = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_revokeRequest, pkiData); + BodyPartID revokeBodyPartId = cmcControlObject.getBodyPartID(); + RevokeRequest revokeRequest = (RevokeRequest) cmcControlObject.getValue(); + // Check issuer name + final X500Name issuerName = revokeRequest.getName(); + if (caService.getCaCertificate().getSubject().equals(issuerName)) { + Date revokeDate = revokeRequest.getInvalidityDate().getDate(); + int reason = revokeRequest.getReason().getValue().intValue(); + BigInteger serialNumber = revokeRequest.getSerialNumber(); + + try { + caService.revokeCertificate(serialNumber, reason, revokeDate); + } catch (Exception ex2) { + throw new CMCCaApiException(ex2.getMessage(), ex2, Arrays.asList(revokeBodyPartId), CMCFailType.badCertId); + } + CMCResponseModel responseModel = new CMCBasicCMCResponseModel( + cmcRequest.getNonce(), + new CMCResponseStatus(CMCStatusType.success, Arrays.asList(revokeBodyPartId)), null, null + ); + return cmcResponseFactory.getCMCResponse(responseModel); + } else { + throw new CMCCaApiException("Revocation request does not match CA issuer name", Arrays.asList(revokeBodyPartId), CMCFailType.badRequest); + } + } catch (Exception ex) { + if (ex instanceof CMCCaApiException) { + throw (CMCCaApiException) ex; + } + throw new CMCCaApiException(ex, new ArrayList<>(), CMCFailType.badRequest); + } + } + + protected CMCResponse processCustomRequest(CMCRequest cmcRequest) throws Exception { + PKIData pkiData = cmcRequest.getPkiData(); + CMCControlObject cmcControlObject = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_regInfo, pkiData); + AdminCMCData adminRequest = (AdminCMCData) cmcControlObject.getValue(); + AdminCMCData adminResponse = getAdminResponse(adminRequest); + CMCResponseModel responseModel = new CMCAdminResponseModel( + cmcRequest.getNonce(), + new CMCResponseStatus(CMCStatusType.success, Arrays.asList(cmcControlObject.getBodyPartID())), + cmcRequest.getCmcRequestType(), + adminResponse + ); + + return cmcResponseFactory.getCMCResponse(responseModel); + } + + protected abstract AdminCMCData getAdminResponse(AdminCMCData adminRequest) throws Exception; + + protected CMCResponse processGetCertRequest(CMCRequest cmcRequest) throws CMCCaApiException { + List requestBodyParts = new ArrayList<>(); + try { + PKIData pkiData = cmcRequest.getPkiData(); + CMCControlObject cmcControlObject = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_getCert, pkiData); + requestBodyParts = Arrays.asList(cmcControlObject.getBodyPartID()); + GetCert getCert = (GetCert) cmcControlObject.getValue(); + X500Name issuerName = (X500Name) getCert.getIssuerName().getName(); + if (caService.getCaCertificate().getSubject().equals(issuerName)) { + CertificateRecord certificateRecord = caService.getCaRepository().getCertificate(getCert.getSerialNumber()); + X509CertificateHolder targetCertificateHolder = new X509CertificateHolder(certificateRecord.getCertificate()); + CMCResponseModel responseModel = new CMCBasicCMCResponseModel( + cmcRequest.getNonce(), + new CMCResponseStatus(CMCStatusType.success, requestBodyParts), + cmcRequest.getCmcRequestType(), null, + Arrays.asList(CAUtils.getCert(targetCertificateHolder)) + ); + return cmcResponseFactory.getCMCResponse(responseModel); + } + } catch (Exception ex) { + throw new CMCCaApiException("Failure to process Get Cert reqeust", ex, requestBodyParts, CMCFailType.badRequest); + } + throw new CMCCaApiException("Get certificate request does not match CA issuer name", requestBodyParts, CMCFailType.badRequest); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/api/impl/DefaultCMCCaApi.java b/src/main/java/se/swedenconnect/ca/cmc/api/impl/DefaultCMCCaApi.java new file mode 100644 index 0000000..2b99091 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/api/impl/DefaultCMCCaApi.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.api.impl; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.cmc.BodyPartID; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; +import org.bouncycastle.asn1.cmc.CertificationRequest; +import org.bouncycastle.asn1.cmc.LraPopWitness; +import org.bouncycastle.asn1.crmf.CertTemplate; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.cert.crmf.CertificateRequestMessage; +import org.bouncycastle.cert.crmf.jcajce.JcaCertificateRequestMessage; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCSException; +import se.swedenconnect.ca.cmc.api.CMCRequestParser; +import se.swedenconnect.ca.cmc.api.CMCResponseFactory; +import se.swedenconnect.ca.cmc.api.data.CMCControlObject; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.engine.ca.issuer.CAService; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.ExtensionModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.GenericExtensionModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.InheritExtensionModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.EncodedCertNameModel; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Default CMC API implementation. This API implementation extends the {@link AbstractAdminCMCCaApi} + * providing default functionality for processing CMC requests. This implementation only provides the functionality + * for creating the Certificate issuing model data used as input for Certificate Issuance. + * + * Modifications of this class may implement other rules, checks or overrides to what extensions or certificate data that is accepted + * in issued certificates based on a CMC request. + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class DefaultCMCCaApi extends AbstractAdminCMCCaApi { + + public DefaultCMCCaApi(CAService caService, + CMCRequestParser cmcRequestParser, CMCResponseFactory cmcResponseFactory) { + super(caService, cmcRequestParser, cmcResponseFactory); + } + + @Override CertificateModel getCertificateModel(CMCRequest cmcRequest) throws Exception { + CertificationRequest certificationRequest = cmcRequest.getCertificationRequest(); + CertificateRequestMessage certificateRequestMessage = cmcRequest.getCertificateRequestMessage(); + + if (certificationRequest != null) { + return getCertificateModelFromPKCS10(certificationRequest); + } + + CMCControlObject lraPWObject = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_lraPOPWitness, cmcRequest.getPkiData()); + LraPopWitness lraPopWitness = (LraPopWitness) lraPWObject.getValue(); + + return getCertificateModelFromCRMF(certificateRequestMessage, lraPopWitness, cmcRequest.getCertReqBodyPartId()); + } + + private CertificateModel getCertificateModelFromCRMF(CertificateRequestMessage certificateRequestMessage, LraPopWitness lraPopWitness, + BodyPartID certReqBodyPartId) throws Exception{ + + // Check POP + if (lraPopWitness == null) { + throw new IOException("Certificate request message format requests must hav LRA POP Witness set"); + } + final List lraPopIdList = Arrays.asList(lraPopWitness.getBodyIds()).stream() + .map(BodyPartID::getID) + .collect(Collectors.toList()); + if (!lraPopIdList.contains(certReqBodyPartId.getID())){ + throw new IOException("No matching LRA POP Witness ID in CRMF request"); + } + + CertTemplate certTemplate = certificateRequestMessage.getCertTemplate(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + PublicKey publicKey = converter.getPublicKey(certTemplate.getPublicKey()); + Extensions extensions = certTemplate.getExtensions(); + ASN1ObjectIdentifier[] extensionOIDs = extensions.getExtensionOIDs(); + List extensionModelList = new ArrayList<>(); + for (ASN1ObjectIdentifier extOid : extensionOIDs) { + Extension extension = extensions.getExtension(extOid); + extensionModelList.add(new GenericExtensionModel( + extension.getExtnId(), + extension.getParsedValue().toASN1Primitive(), + extension.isCritical() + )); + } + + CertificateModel certificateModel = CertificateModel.builder() + .publicKey(publicKey) + .subject(new EncodedCertNameModel(certTemplate.getSubject())) + .extensionModels(extensionModelList) + .build(); + return certificateModel; + } + + private CertificateModel getCertificateModelFromPKCS10(CertificationRequest certificationRequest) throws Exception { + PKCS10CertificationRequest pkcs10Request = new PKCS10CertificationRequest(certificationRequest.getEncoded(ASN1Encoding.DER)); + PublicKey publicKey = validatePkcs10Signature(pkcs10Request); + pkcs10Request.getSubject(); + + Attribute[] p10ExtAttributes = pkcs10Request.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); + List extensionModelList = new ArrayList<>(); + if (p10ExtAttributes != null && p10ExtAttributes.length > 0) { + Attribute attribute = Attribute.getInstance(p10ExtAttributes[0]); + ASN1Sequence extSequence = ASN1Sequence.getInstance(attribute.getAttrValues().getObjectAt(0)); + Iterator iterator = extSequence.iterator(); + while (iterator.hasNext()) { + Extension extension = Extension.getInstance(iterator.next()); + extensionModelList.add(new GenericExtensionModel( + extension.getExtnId(), + extension.getParsedValue().toASN1Primitive(), + extension.isCritical() + )); + } + } + + CertificateModel certificateModel = CertificateModel.builder() + .publicKey(publicKey) + .subject(new EncodedCertNameModel(pkcs10Request.getSubject())) + .extensionModels(extensionModelList) + .build(); + return certificateModel; + } + + private PublicKey validatePkcs10Signature(PKCS10CertificationRequest pkcs10Request) + throws IOException, OperatorCreationException, PKCSException { + JcaContentVerifierProviderBuilder builder = new JcaContentVerifierProviderBuilder().setProvider("BC"); + boolean signatureValid = pkcs10Request.isSignatureValid(builder.build(pkcs10Request.getSubjectPublicKeyInfo())); + if (signatureValid) { + return BouncyCastleProvider.getPublicKey(pkcs10Request.getSubjectPublicKeyInfo()); + } + throw new IOException("Invalid PKCS10 signature"); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/AuthorizedCmcOperation.java b/src/main/java/se/swedenconnect/ca/cmc/auth/AuthorizedCmcOperation.java new file mode 100644 index 0000000..7bf45a2 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/AuthorizedCmcOperation.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public enum AuthorizedCmcOperation { + issue, revoke, read + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/CMCAuthorizationException.java b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCAuthorizationException.java new file mode 100644 index 0000000..4797e92 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCAuthorizationException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCAuthorizationException extends Exception { + public CMCAuthorizationException() { + } + + public CMCAuthorizationException(String message) { + super(message); + } + + public CMCAuthorizationException(String message, Throwable cause) { + super(message, cause); + } + + public CMCAuthorizationException(Throwable cause) { + super(cause); + } + + public CMCAuthorizationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/CMCReplayChecker.java b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCReplayChecker.java new file mode 100644 index 0000000..cf1a687 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCReplayChecker.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +import org.bouncycastle.asn1.cmc.PKIData; +import org.bouncycastle.cms.CMSSignedData; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; + +import java.io.IOException; +import java.util.Date; + +/** + * Interface for implementation of a replay checker used by the CMC parser to determine if a CMC request is new and not + * a replay of an old request. + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public interface CMCReplayChecker { + + /** + * Validates a CMC request against replay according to a defined policy + * @param nonce the nonce of the CMC request to validate + * @param signingTime The signing time collected from the CMS signature signed attributes (1.2.840.113549.1.9.5) + * @throws IOException if a violation of the replay protection policy is detected + */ + void validate(CMSSignedData cmsSignedData) throws IOException; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/CMCUtils.java b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCUtils.java new file mode 100644 index 0000000..eb736ae --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCUtils.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.CMSAttributes; +import org.bouncycastle.asn1.cms.Time; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.ExtensionsGenerator; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.crmf.CertificateRequestMessageBuilder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.*; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.util.Store; +import se.swedenconnect.ca.cmc.api.data.CMCControlObject; +import se.swedenconnect.ca.cmc.api.data.CMCControlObjectID; +import se.swedenconnect.ca.cmc.api.data.CMCResponse; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.response.CAInformation; +import se.swedenconnect.ca.cmc.model.admin.response.CertificateData; +import se.swedenconnect.ca.engine.ca.attribute.AttributeValueEncoder; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.ExtensionModel; +import se.swedenconnect.ca.engine.utils.CAUtils; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility functions for parsing and creating CMC messages + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class CMCUtils { + + public static final SecureRandom RNG = new SecureRandom(); + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** + * Create a CRMF request message builder for a CRMF certificate request + * @param requestId the ID of the created request + * @param certificateModel model holding data about the certificate to be issued + * @param attributeValueEncoder encoder for attribute values + * @return CRMF request message builder + * @throws IOException on error creating the builder + */ + public static CertificateRequestMessageBuilder getCRMFRequestMessageBuilder(BodyPartID requestId, CertificateModel certificateModel, AttributeValueEncoder attributeValueEncoder) + throws IOException { + CertificateRequestMessageBuilder crmfBuilder = new CertificateRequestMessageBuilder(new BigInteger(String.valueOf(requestId.getID()))); + crmfBuilder.setSubject(CAUtils.getX500Name(certificateModel.getSubject(), attributeValueEncoder)); + + SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance( + ASN1Sequence.getInstance(certificateModel.getPublicKey().getEncoded())); + crmfBuilder.setPublicKey(subjectPublicKeyInfo); + + List extensionModels = certificateModel.getExtensionModels(); + for (ExtensionModel extensionModel: extensionModels){ + List extensions = extensionModel.getExtensions(); + for (Extension extension: extensions){ + crmfBuilder.addExtension(extension.getExtnId(), extension.isCritical(), extension.getParsedValue()); + } + } + return crmfBuilder; + } + + /** + * Creates a PKCS10 request + * @param certificateModel data about the certificate to be requested + * @param signer the signer of the PKCS10 request + * @param attributeValueEncoder attribute value encoder + * @return PKCS10 request + * @throws IOException on errors creating the request + */ + public static CertificationRequest getCertificationRequest(CertificateModel certificateModel, ContentSigner signer, AttributeValueEncoder attributeValueEncoder) + throws IOException { + + X500Name subjectX500Name = CAUtils.getX500Name(certificateModel.getSubject(), attributeValueEncoder); + SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance( + ASN1Sequence.getInstance(certificateModel.getPublicKey().getEncoded())); + + PKCS10CertificationRequestBuilder p10ReqBuilder = new PKCS10CertificationRequestBuilder(subjectX500Name, subjectPublicKeyInfo); + ExtensionsGenerator extGen = new ExtensionsGenerator(); + List extensionModels = certificateModel.getExtensionModels(); + for (ExtensionModel extensionModel: extensionModels){ + List extensions = extensionModel.getExtensions(); + for (Extension extension: extensions){ + extGen.addExtension(extension); + } + } + p10ReqBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate()); + PKCS10CertificationRequest pkcs10 = p10ReqBuilder.build(signer); + return CertificationRequest.getInstance(pkcs10.toASN1Structure().toASN1Primitive()); + } + + public static byte[] signEncapsulatedCMSContent(ASN1ObjectIdentifier contentType, ASN1Encodable content, List signerCertChain, ContentSigner signer) throws IOException { + try { + final Store certs = new JcaCertStore(signerCertChain); + final CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + final org.bouncycastle.asn1.x509.Certificate cert = org.bouncycastle.asn1.x509.Certificate.getInstance( + ASN1Primitive.fromByteArray(signerCertChain.get(0).getEncoded())); + //final ContentSigner signer = new JcaContentSignerBuilder(CAAlgorithmRegistry.getSigAlgoName(algorithm)).build(signKey); + final JcaSignerInfoGeneratorBuilder builder = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()); + gen.addSignerInfoGenerator(builder.build(signer, new X509CertificateHolder(cert))); + gen.addCertificates(certs); + //final CMSTypedData encapsulatedContent = new PKCS7ProcessableObject(contentType, content); + final CMSProcessableByteArray encapsulatedContent = new CMSProcessableByteArray(contentType, content.toASN1Primitive().getEncoded(ASN1Encoding.DER)); + final CMSSignedData resultSignedData = gen.generate(encapsulatedContent, true); + return resultSignedData.toASN1Structure().getEncoded(ASN1Encoding.DL); + } + catch (GeneralSecurityException | CMSException | OperatorCreationException e) { + final String msg = String.format("Failed to sign content - %s", e.getMessage()); + log.error("{}", msg, e); + throw new IOException(msg, e); + } + } + + public static CMCControlObject getCMCControlObject(ASN1ObjectIdentifier asn1controlOid, PKIResponse pkiResponse) + throws IOException { + return getCMCControlObject(asn1controlOid, getResponseControlSequence(pkiResponse)); + + } + public static CMCControlObject getCMCControlObject(ASN1ObjectIdentifier asn1controlOid, PKIData pkiData) + throws IOException { + TaggedAttribute[] controlSequence = pkiData.getControlSequence(); + return getCMCControlObject(asn1controlOid, controlSequence); + } + + private static CMCControlObject getCMCControlObject(ASN1ObjectIdentifier asn1controlOid, TaggedAttribute[] controlSequence) + throws IOException { + CMCControlObjectID controlOid = CMCControlObjectID.getControlObjectID(asn1controlOid); + CMCControlObject.CMCControlObjectBuilder resultBuilder = CMCControlObject.builder().type(controlOid); + for (TaggedAttribute controlAttr : controlSequence){ + ASN1ObjectIdentifier attrType = controlAttr.getAttrType(); + if (attrType != null && attrType.equals(controlOid.getOid())){ + resultBuilder + .bodyPartID(controlAttr.getBodyPartID()) + .value(getRequestControlValue(controlOid, controlAttr.getAttrValues())); + } + } + return resultBuilder.build(); + } + + private static Object getRequestControlValue(CMCControlObjectID controlOid, ASN1Set controlAttrVals) + throws IOException { + Object controlValue = getControlValue(controlOid, controlAttrVals); + if (CMCControlObjectID.regInfo.equals(controlOid) || CMCControlObjectID.responseInfo.equals(controlOid)){ + byte[] dataBytes = (byte[]) controlValue; + return getbytesOrJsonObject(dataBytes, AdminCMCData.class); + } + return controlValue; + } + + private static Object getbytesOrJsonObject(byte[] regInfoBytes, Class dataClass) { + try { + return OBJECT_MAPPER.readValue(regInfoBytes, dataClass); + } catch (Exception ex){ + return regInfoBytes; + } + } + + private static Object getControlValue(CMCControlObjectID controlOid, ASN1Set controlAttrVals) + throws IOException { + + try { + if (controlAttrVals.size()==0){ + log.debug("No values - Returning null"); + return null; + } + ASN1Encodable firstObject = controlAttrVals.getObjectAt(0); + if (firstObject == null){ + log.debug("No control value - Returning null"); + return null; + } + + if (CMCControlObjectID.regInfo.equals(controlOid) + || CMCControlObjectID.responseInfo.equals(controlOid) + || CMCControlObjectID.senderNonce.equals(controlOid) + || CMCControlObjectID.recipientNonce.equals(controlOid) + ){ + return ASN1OctetString.getInstance(firstObject).getOctets(); + } + if (CMCControlObjectID.getCert.equals(controlOid)){ + return GetCert.getInstance(firstObject); + } + if (CMCControlObjectID.lraPOPWitness.equals(controlOid)){ + return LraPopWitness.getInstance(firstObject); + } + if (CMCControlObjectID.revokeRequest.equals(controlOid)){ + return RevokeRequest.getInstance(firstObject); + } + if (CMCControlObjectID.statusInfoV2.equals(controlOid)){ + return CMCStatusInfoV2.getInstance(firstObject); + } + } catch (Exception ex){ + throw new IOException("Error extracting CMC control value", ex); + } + log.debug("Unsupported CMC control message {} - returning null", controlOid); + return null; + } + + /** + * Return the status code value of CMCStatus + * @param cmcStatus CMCStatus + * @return integer value + * @throws Exception On illegal status value content + */ + public static int getCMCStatusCode(CMCStatus cmcStatus) throws Exception{ + ASN1Integer cmcStatusAsn1Int = (ASN1Integer) cmcStatus.toASN1Primitive(); + return cmcStatusAsn1Int.intPositiveValueExact(); + } + + /** + * Get the control sequence array from a CMC PKI Response + * @param pkiResponse CMC PKI Response + * @return control data sequence in the form of an array of {@link TaggedAttribute} + */ + public static TaggedAttribute[] getResponseControlSequence(PKIResponse pkiResponse){ + List attributeList = new ArrayList<>(); + ASN1Sequence controlSequence = pkiResponse.getControlSequence(); + if (controlSequence.size() > 0) { + Iterator iterator = controlSequence.iterator(); + while (iterator.hasNext()){ + TaggedAttribute csAttr = TaggedAttribute.getInstance(iterator.next()); + attributeList.add(csAttr); + } + } + return attributeList.toArray(new TaggedAttribute[0]); + } + + /** + * Return a list of certificate bytes representing a list of X509 Certificates + * @param certificateList list of certificates + * @return list of certificate bytes + * @throws CertificateEncodingException on certificate encoding errors + */ + public static List getCertByteList(List certificateList) throws CertificateEncodingException { + List certByteList = new ArrayList<>(); + for (X509Certificate cert: certificateList){ + certByteList.add(cert.getEncoded()); + } + return certByteList; + } + + /** + * Return a list of certificate bytes representing a list of X509 Certificates + * @param certificateList list of certificates + * @return list of certificate bytes + * @throws IOException on certificate encoding errors + */ + public static List getCerHolderByteList(List certificateList) throws IOException { + List certByteList = new ArrayList<>(); + for (X509CertificateHolder cert: certificateList){ + certByteList.add(cert.getEncoded()); + } + return certByteList; + } + + public static CAInformation getCAInformation(CMCResponse cmcResponse) throws IOException { + final AdminCMCData adminCMCData = getAdminCMCData(cmcResponse); + return CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), CAInformation.class); + } + public static AdminCMCData getAdminCMCData(CMCResponse cmcResponse) throws IOException { + final CMCControlObject responseControlObject = getResponseControlObject(cmcResponse, CMCObjectIdentifiers.id_cmc_responseInfo); + return (AdminCMCData) responseControlObject.getValue(); + } + public static CMCControlObject getResponseControlObject(CMCResponse cmcResponse, ASN1ObjectIdentifier controlObjOid) throws IOException { + final TaggedAttribute[] taggedAttributes = CMCUtils.getResponseControlSequence(cmcResponse.getPkiResponse()); + return CMCUtils.getCMCControlObject(controlObjOid, taggedAttributes); + } + + public static List getAllSerials(CMCResponse cmcResponse) throws IOException { + final AdminCMCData adminCMCData = getAdminCMCData(cmcResponse); + final List serials = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), new TypeReference<>() {}); + return serials.stream().map(s -> new BigInteger(s, 16)).collect(Collectors.toList()); + } + + public static List getCertList(CMCResponse cmcResponse) throws IOException { + final AdminCMCData adminCMCData = getAdminCMCData(cmcResponse); + return CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), new TypeReference<>() {}); + } + + /** + * Get the value of the signed signingTime attribute from a CMS signed CMC message + * @param cmsContentInfo CMS content info bytes + * @return signing time attribute value if present, or null + * @throws CMSException error parsing CMS data + */ + public static Date getSigningTime(byte[] cmsContentInfo) throws CMSException { + return getSigningTime(new CMSSignedData(cmsContentInfo)); + } + + /** + * Get the value of the signed signingTime attribute from a CMS signed CMC message + * @param signedData CMS signed data + * @return signing time attribute value if present, or null + */ + public static Date getSigningTime(CMSSignedData signedData) { + final SignerInformation signerInformation = signedData.getSignerInfos().iterator().next(); + final Attribute signingTimeAttr = signerInformation.getSignedAttributes().get(CMSAttributes.signingTime); + return signingTimeAttr == null + ? null + : Time.getInstance(signingTimeAttr.getAttrValues().getObjectAt(0)).getDate(); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidationException.java b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidationException.java new file mode 100644 index 0000000..df26a73 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidationException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +import lombok.Getter; + +/** + * Exception thrown during CMC validation + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCValidationException extends RuntimeException { + + public CMCValidationException() { + } + + public CMCValidationException(String message) { + super(message); + } + + public CMCValidationException(String message, Throwable cause) { + super(message, cause); + } + + public CMCValidationException(Throwable cause) { + super(cause); + } + + public CMCValidationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidationResult.java b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidationResult.java new file mode 100644 index 0000000..984743d --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidationResult.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSSignedData; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Data class holding CMC validation data + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CMCValidationResult { + + /** Indicates if the CMC message is valid and originates from an authorized source */ + private boolean valid; + /** Indicates if the response is a simple response */ + private boolean simpleResponse; + /** Holding the signed data structure of the CMC message */ + private CMSSignedData signedData; + /** The content type of the CMS signature */ + private ASN1ObjectIdentifier contentType; + /** The validated certificate path */ + private List signerCertificatePath; + /** Optional exception thrown during validation */ + private Exception exception; + /** Optional error message */ + private String errorMessage; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidator.java b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidator.java new file mode 100644 index 0000000..d41c034 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/CMCValidator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth; + +import lombok.NoArgsConstructor; + +/** + * Interface for a CMC message validator that validates the signature on the CMC message as well + * as that the originator is authorized to send this request. + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public interface CMCValidator { + + /** + * Validates the signature on a CMC against a defined trust configuration + * @param cmcMessage the CMC message to validate + * @return Validation result + */ + CMCValidationResult validateCMC(byte[] cmcMessage); + + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/impl/AbstractCMCValidator.java b/src/main/java/se/swedenconnect/ca/cmc/auth/impl/AbstractCMCValidator.java new file mode 100644 index 0000000..3e4f58d --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/impl/AbstractCMCValidator.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth.impl; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.cms.SignedData; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationVerifier; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import se.swedenconnect.ca.cmc.auth.CMCAuthorizationException; +import se.swedenconnect.ca.cmc.auth.CMCValidationException; +import se.swedenconnect.ca.cmc.auth.CMCValidationResult; +import se.swedenconnect.ca.cmc.auth.CMCValidator; + +import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * Abstract implementation of the CMC Validator interface + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public abstract class AbstractCMCValidator implements CMCValidator { + + public AbstractCMCValidator() { + } + + @Override public CMCValidationResult validateCMC(byte[] cmcMessage) { + + CMCValidationResult result = new CMCValidationResult(); + if (isSimpleCMCResponse(result, cmcMessage)) { + return result; + } + + try { + CMSSignedData cmsSignedData = new CMSSignedData(cmcMessage); + ASN1ObjectIdentifier contentType = cmsSignedData.getSignedContent().getContentType(); + if (contentType.equals(CMCObjectIdentifiers.id_cct_PKIData) || contentType.equals(CMCObjectIdentifiers.id_cct_PKIResponse)) { + result.setContentType(contentType); + } + else { + result.setValid(false); + result.setErrorMessage("Illegal CMC data content type"); + result.setException(new IOException("Illegal CMC data content type")); + return result; + } + result.setSignedData(cmsSignedData); + + List trustedSignerCertChain = verifyCMSSignature(cmsSignedData); + verifyAuthorization(trustedSignerCertChain.get(0), contentType, cmsSignedData); + // Set result conclusion + result.setSignerCertificatePath(trustedSignerCertChain); + result.setSimpleResponse(false); + result.setValid(true); + } + catch (CMCAuthorizationException aex) { + result.setValid(false); + result.setException(aex); + result.setErrorMessage(aex.getMessage()); + } + catch (CMCValidationException vex) { + result.setValid(false); + result.setException(vex); + result.setErrorMessage("CMC signature validation failed: " + vex.getMessage()); + } catch (Exception ex) { + result.setValid(false); + result.setException(ex); + result.setErrorMessage("Error parsing CMC message: " + ex.toString()); + } + + return result; + } + + /** + * Verifies the CMS signature + * @param cmsSignedData the signed data to verify + * @return The signing certificate chain if the verification was successful + * @throws IOException if signature validation failed + */ + protected abstract List verifyCMSSignature(CMSSignedData cmsSignedData) throws CMCValidationException; + + /** + * Verifies the authorization of the signer to provide this CMC message or request the specified operations + * @param signer the verified signer of this CMC message + * @param contentType the CMC encapsulated data content type + * @param cmsSignedData the CMC message signed data to be authorized + * @throws Exception if authorization fails + */ + protected abstract void verifyAuthorization(X509CertificateHolder signer, ASN1ObjectIdentifier contentType, CMSSignedData cmsSignedData) throws + CMCAuthorizationException; + + + private boolean isSimpleCMCResponse(CMCValidationResult result, byte[] cmcMessage) { + List certificateList = new ArrayList<>(); + + try { + ASN1InputStream ain = new ASN1InputStream(cmcMessage); + ContentInfo cmsContentInfo = ContentInfo.getInstance(ain.readObject()); + if (!cmsContentInfo.getContentType().equals(CMSObjectIdentifiers.signedData)) { + // The Body of the CMS ContentInfo MUST be SignedData + return false; + } + SignedData signedData = SignedData.getInstance(cmsContentInfo.getContent()); + ASN1Set signerInfos = signedData.getSignerInfos(); + if (signerInfos != null && signerInfos.size()>0){ + // This is not a simple response if signerInfos is present + return false; + } + // This is a simple response + return true; + } catch (Exception ex){ + log.debug("Failed to parse response as valid CMS data"); + return false; + } + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/impl/DefaultCMCReplayChecker.java b/src/main/java/se/swedenconnect/ca/cmc/auth/impl/DefaultCMCReplayChecker.java new file mode 100644 index 0000000..ed2cc2e --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/impl/DefaultCMCReplayChecker.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth.impl; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1UTCTime; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; +import org.bouncycastle.asn1.cmc.PKIData; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.CMSAttributes; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.asn1.cms.Time; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import se.swedenconnect.ca.cmc.api.data.CMCControlObjectID; +import se.swedenconnect.ca.cmc.auth.CMCReplayChecker; +import se.swedenconnect.ca.cmc.auth.CMCUtils; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Default implementation of a replay checker + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class DefaultCMCReplayChecker implements CMCReplayChecker { + + private static final Date startupTime; + + static { + RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); + startupTime = new Date(runtimeMXBean.getStartTime()); + } + + private List nonceList = new ArrayList<>(); + long maxAgeMillis; + long retentionMillis; + long futureTimeSkewMillis; + + + public DefaultCMCReplayChecker(int maxAgeSec, long retentionSec, long futureTimeSkewSec) { + this.maxAgeMillis = 1000L * maxAgeSec; + this.retentionMillis = 1000L * retentionSec; + this.futureTimeSkewMillis = 1000L * futureTimeSkewSec; + log.info("Replay checker created with system start time = {}, max age sec={}, retention sec={}, future time skew sec={}", startupTime, maxAgeSec, retentionSec, futureTimeSkewSec); + } + public DefaultCMCReplayChecker(int maxAgeSec, long retentionSec) { + this (maxAgeSec, retentionSec, 60); + } + + public DefaultCMCReplayChecker() { + this(120, 200, 60); + } + + @Override public void validate(CMSSignedData signedData) throws IOException { + try { + consolidateReplayData(); + PKIData pkiData = PKIData.getInstance(new ASN1InputStream((byte[]) signedData.getSignedContent().getContent()).readObject()); + Date messageTime = CMCUtils.getSigningTime(signedData); + Date notBefore = new Date(System.currentTimeMillis() - maxAgeMillis); + Date notAfter = new Date(System.currentTimeMillis() + futureTimeSkewMillis); + if (messageTime == null){ + throw new IOException("Replay check failed: Message time is missing in CMC request"); + } + if (messageTime.before(startupTime)){ + // We do not allow under any circumstances a message created before startup time as we have no knowledge of what happened before this instant. + throw new IOException("Replay check failed: Request older than system startup time"); + } + if (messageTime.before(notBefore)) { + throw new IOException("Replay check failed: Request is to lod"); + } + if (messageTime.after(notAfter)){ + throw new IOException("Replay check failed: Request time in future time"); + } + byte[] nonce = (byte[]) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_senderNonce, pkiData).getValue(); + if (nonce == null){ + throw new IOException("Replay check failed: Request nonce is missing"); + } + + if (nonceList.stream().anyMatch(replayData -> Arrays.equals(nonce, replayData.getNonce()))){ + throw new IOException("Replay check failed: Replay of request nonce"); + } + nonceList.add(new ReplayData (nonce, messageTime)); + + } catch (Exception ex) { + if (ex instanceof IOException){ + throw (IOException) ex; + } + throw new IOException("Error processing replay data - Replay check failed", ex); + } + } + + private void consolidateReplayData() { + Date maxAge = new Date(System.currentTimeMillis() - retentionMillis); + nonceList = nonceList.stream() + .filter(replayData -> replayData.getMessageTime().after(maxAge)) + .collect(Collectors.toList()); + } + + @Getter + @AllArgsConstructor + public static class ReplayData { + byte[] nonce; + Date messageTime; + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/auth/impl/DefaultCMCValidator.java b/src/main/java/se/swedenconnect/ca/cmc/auth/impl/DefaultCMCValidator.java new file mode 100644 index 0000000..9ab4745 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/auth/impl/DefaultCMCValidator.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.auth.impl; + +import lombok.Setter; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; +import org.bouncycastle.asn1.cmc.PKIData; +import org.bouncycastle.asn1.cmc.TaggedRequest; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationVerifier; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import se.swedenconnect.ca.cmc.api.data.CMCControlObject; +import se.swedenconnect.ca.cmc.auth.AuthorizedCmcOperation; +import se.swedenconnect.ca.cmc.auth.CMCAuthorizationException; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.auth.CMCValidationException; + +import java.security.PublicKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * Provides a default CMC validator that validates the CMC signature based on a set of trusted certificates. + * This validator requires the CMC to be signed by a single certificate. + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class DefaultCMCValidator extends AbstractCMCValidator{ + + private final List trustedCMCSigners; + @Setter private Map> clientAuthorizationMap; + + /** + * Constructor for a default CMC Signature validator + * @param trustedCMCSigners trusted CMC signer certificates + * @throws CertificateEncodingException on errors parsing the provided certificates + */ + public DefaultCMCValidator(X509Certificate... trustedCMCSigners) throws CertificateEncodingException { + this.trustedCMCSigners = new ArrayList(); + for (X509Certificate cert : trustedCMCSigners) { + this.trustedCMCSigners.add(new JcaX509CertificateHolder(cert)); + } + } + + /** + * Constructor for a default CMC Signature validator + * @param trustedCMCSigners trusted CMC signer certificates + */ + public DefaultCMCValidator(X509CertificateHolder... trustedCMCSigners) { + this.trustedCMCSigners = Arrays.asList(trustedCMCSigners); + } + + /** {@inheritDoc} */ + @Override protected List verifyCMSSignature(CMSSignedData cmsSignedData) + throws CMCValidationException { + try { + Collection certsInCMS = cmsSignedData.getCertificates().getMatches(null); + X509CertificateHolder trustedSignerCert = getTrustedSignerCert(certsInCMS); + SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder().build(trustedSignerCert); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + final boolean verify = signerInformation.verify(signerInformationVerifier); + if (!verify) { + throw new RuntimeException("CMC Signature validation failed"); + } + return Arrays.asList(trustedSignerCert); + } catch (Exception ex){ + throw new CMCValidationException(ex.getMessage(), ex); + } + } + + @Override protected void verifyAuthorization(X509CertificateHolder signer, ASN1ObjectIdentifier contentType, CMSSignedData signedData) + throws CMCAuthorizationException { + if (clientAuthorizationMap == null) { + // No client authorization map is set. Approve authorization + return; + } + if (!CMCObjectIdentifiers.id_cct_PKIData.equals(contentType)){ + // Authorization only applies to CMC requests in this implementation. Approve. + return; + } + final List authorizedCmcOperationList = clientAuthorizationMap.get(signer); + try { + // Base authorization read must allways be set + if (!authorizedCmcOperationList.contains(AuthorizedCmcOperation.read)){ + throw new CMCAuthorizationException("CMC client not authorized to access the requested CA service"); + } + // Check if there is a certificate issuing request present + PKIData pkiData = PKIData.getInstance(new ASN1InputStream((byte[]) signedData.getSignedContent().getContent()).readObject()); + TaggedRequest[] reqSequence = pkiData.getReqSequence(); + if (reqSequence.length > 0) { + if (!authorizedCmcOperationList.contains(AuthorizedCmcOperation.issue)){ + throw new CMCAuthorizationException("CMC client not authorized to issue certificates"); + } + } + // Check if there is a revoke request present + final CMCControlObject revokeControlAttribute = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_revokeRequest, pkiData); + if (revokeControlAttribute != null && revokeControlAttribute.getValue() != null){ + if (!authorizedCmcOperationList.contains(AuthorizedCmcOperation.revoke)){ + throw new CMCAuthorizationException("CMC client not authorized to revoke certificates"); + } + } + } + catch (CMCAuthorizationException authorizationException) { + throw authorizationException; + } + catch (Exception ex) { + throw new CMCAuthorizationException("Failure to process CMC client authorization check", ex); + } + } + + private X509CertificateHolder getTrustedSignerCert(Collection certsInCMS) { + if (trustedCMCSigners == null | trustedCMCSigners.isEmpty()) { + throw new IllegalArgumentException("This CMC verifier has no trusted CMC signer certificates"); + } + if (certsInCMS == null || certsInCMS.size() ==0 ){ + throw new IllegalArgumentException("No signature certificates found in CMC signature"); + } + Iterator iterator = certsInCMS.iterator(); + while (iterator.hasNext()) { + final X509CertificateHolder cmsCert = iterator.next(); + for (X509CertificateHolder trustedCMCSigner : trustedCMCSigners) { + if (trustedCMCSigner.equals(cmsCert)) { + return trustedCMCSigner; + } + } + } + throw new IllegalArgumentException("No trusted certificate found in signed CMC"); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/admin/AdminCMCData.java b/src/main/java/se/swedenconnect/ca/cmc/model/admin/AdminCMCData.java new file mode 100644 index 0000000..bc590c3 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/admin/AdminCMCData.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.admin; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Data class holding any Admin request or response data in a CMC request regInfo attribute or a CMC response responseInfo attribute + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AdminCMCData { + + /** Type of admin request */ + private AdminRequestType adminRequestType; + /** Admin request/response data */ + private String data; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/admin/AdminRequestType.java b/src/main/java/se/swedenconnect/ca/cmc/model/admin/AdminRequestType.java new file mode 100644 index 0000000..447b484 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/admin/AdminRequestType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.admin; + +/** + * Enumeration of supported admin request types + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public enum AdminRequestType { + /** Request for information about the responding CA */ + caInfo, + /** Request to obtain a list of issued certificates from the CA */ + listCerts, + /** Request to obtain a list of all current certificate serial numbers of certificates in the CA database */ + allCertSerials +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/admin/request/ListCerts.java b/src/main/java/se/swedenconnect/ca/cmc/model/admin/request/ListCerts.java new file mode 100644 index 0000000..5722dcc --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/admin/request/ListCerts.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.admin.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import se.swedenconnect.ca.engine.ca.repository.SortBy; + +/** + * Date for a list certificates admin request + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ListCerts { + + /** indicates of the returned certificates should be limited to currently valid and not revoked certificates */ + private boolean notRevoked; + /** Indicates the preferred sort order */ + private SortBy sortBy; + /** The page size specifying the number of certificates to be returned, if available */ + private int pageSize; + /** The index of the page of certificates to return */ + private int pageIndex; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/admin/response/CAInformation.java b/src/main/java/se/swedenconnect/ca/cmc/model/admin/response/CAInformation.java new file mode 100644 index 0000000..a5d7cc5 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/admin/response/CAInformation.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.admin.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Data class for information about the CA providing this CMC API + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CAInformation { + + /** The number of issued certificates in the database */ + private int certificateCount; + /** The number of non revoked certificates in the database */ + private int validCertificateCount; + /** The CA certificate chain */ + List certificateChain; + /** The optional OCSP certificate used by the OCSP responder of this CA */ + byte[] ocspCertificate; + /** The location of the CRL of this CA service */ + List crlDpURLs; + /** The URL to the OCSP responder of this CA if present */ + String ocspResponserUrl; + /** The algorithm used by this CA to sign certificates */ + String caAlgorithm; + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/admin/response/CertificateData.java b/src/main/java/se/swedenconnect/ca/cmc/model/admin/response/CertificateData.java new file mode 100644 index 0000000..689335b --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/admin/response/CertificateData.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.admin.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Dataclass for a certificate record in the CA database + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CertificateData { + + /** The bytes of the certificate in the record */ + private byte[] certificate; + /** Indicates of the certificate is currently revoked */ + private boolean revoked; + /** Revocation reason if the certificate is revoked */ + private int revocationReason; + /** Revocation date if the certificate is revoked */ + private long revocationDate; +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/CMCRequestModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/CMCRequestModel.java new file mode 100644 index 0000000..620c093 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/CMCRequestModel.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request; + +import org.bouncycastle.operator.ContentSigner; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for the CMC Request model + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public interface CMCRequestModel { + + /** + * Gets the request nonce + * @return request nonce + */ + byte[] getNonce(); + + /** + * Gets the registration info data. Each request type identifies the syntax of this parameter + * @return registration info data + */ + byte[] getRegistrationInfo(); + + /** + * The type of request + * @return cmc request type + */ + CMCRequestType getCmcRequestType(); + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/CMCRequestType.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/CMCRequestType.java new file mode 100644 index 0000000..0bb5433 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/CMCRequestType.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request; + +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; + +/** + * Enumeration of CMC request types + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public enum CMCRequestType { + /** A request to issue a certificate */ + issueCert, + /** A request to revoke a certificate */ + revoke, + /** A custom Admin request using the {@link AdminCMCData} to further specify the type of admin request*/ + admin, + /** A request to get a particular certificate from the CA database based on the serial number of the certificate */ + getCert +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/AbstractCMCRequestModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/AbstractCMCRequestModel.java new file mode 100644 index 0000000..a4c7619 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/AbstractCMCRequestModel.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request.impl; + +import lombok.Getter; +import lombok.Setter; +import org.bouncycastle.operator.ContentSigner; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.request.CMCRequestModel; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Abstract implementation of the CMC request model + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public abstract class AbstractCMCRequestModel implements CMCRequestModel { + + private static final SecureRandom RNG = CMCUtils.RNG; + + public AbstractCMCRequestModel(CMCRequestType cmcRequestType) { + this(cmcRequestType, null); + } + + public AbstractCMCRequestModel(CMCRequestType cmcRequestType, byte[] registrationInfo) { + this.registrationInfo = registrationInfo; + this.cmcRequestType = cmcRequestType; + this.nonce = new byte[128]; + RNG.nextBytes(nonce); + } + + @Getter @Setter protected byte[] nonce; + @Getter @Setter protected byte[] registrationInfo; + @Getter protected CMCRequestType cmcRequestType; + + + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCAdminRequestModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCAdminRequestModel.java new file mode 100644 index 0000000..9287b82 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCAdminRequestModel.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Getter; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; + +import java.io.IOException; + +/** + * CMC Revocation request model + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Getter +public class CMCAdminRequestModel extends AbstractCMCRequestModel { + public CMCAdminRequestModel(AdminCMCData adminRequestData) + throws IOException { + super(CMCRequestType.admin, getReqInfo(adminRequestData)); + } + + private static byte[] getReqInfo(AdminCMCData adminCMCData) throws IOException { + try { + return CMCUtils.OBJECT_MAPPER.writeValueAsBytes(adminCMCData); + } + catch (JsonProcessingException e) { + throw new IOException("Unable to convert admin request data to JSON", e); + } + } +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCCertificateRequestModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCCertificateRequestModel.java new file mode 100644 index 0000000..b166408 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCCertificateRequestModel.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request.impl; + +import lombok.*; +import org.bouncycastle.operator.ContentSigner; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Model for creating a CMC Certificate request + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Getter +public class CMCCertificateRequestModel extends AbstractCMCRequestModel { + + public CMCCertificateRequestModel(CertificateModel certificateModel, String profile) { + super(CMCRequestType.issueCert, profile != null ? profile.getBytes(StandardCharsets.UTF_8) : null); + this.certificateModel = certificateModel; + this.lraPopWitness = true; + } + + public CMCCertificateRequestModel(CertificateModel certificateModel, String profile, + PrivateKey certReqPrivate, String p10Algorithm) { + super(CMCRequestType.issueCert,profile != null ? profile.getBytes(StandardCharsets.UTF_8) : null); + this.certificateModel = certificateModel; + this.certReqPrivate = certReqPrivate; + this.p10Algorithm = p10Algorithm; + this.lraPopWitness = false; + } + + /** Certificate request model */ + private CertificateModel certificateModel; + /** Private key of the requested certificate used to sign PKCS#10 requests */ + private PrivateKey certReqPrivate; + /** Algorithm URI identifier for the algorithm used to sign the pkcs10 request */ + private String p10Algorithm; + /** Boolean to indicate if the requester has verified proof-of-possession of certified private key */ + private boolean lraPopWitness; +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCGetCertRequestModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCGetCertRequestModel.java new file mode 100644 index 0000000..a83755d --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCGetCertRequestModel.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request.impl; + +import lombok.Getter; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.operator.ContentSigner; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * CMC Revocation request model + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Getter +public class CMCGetCertRequestModel extends AbstractCMCRequestModel { + public CMCGetCertRequestModel(BigInteger serialNumber, + X500Name issuerName) { + super(CMCRequestType.getCert); + this.serialNumber = serialNumber; + this.issuerName = issuerName; + } + private X500Name issuerName; + private BigInteger serialNumber; +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCRevokeRequestModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCRevokeRequestModel.java new file mode 100644 index 0000000..02164ee --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/request/impl/CMCRevokeRequestModel.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.request.impl; + +import lombok.Getter; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.operator.ContentSigner; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +/** + * CMC Revocation request model + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Getter +public class CMCRevokeRequestModel extends AbstractCMCRequestModel { + public CMCRevokeRequestModel(BigInteger serialNumber, int reason, Date revocationDate, + X500Name issuerName) { + super(CMCRequestType.revoke); + this.serialNumber = serialNumber; + this.reason = reason; + this.revocationDate = revocationDate; + this.issuerName = issuerName; + } + private X500Name issuerName; + private BigInteger serialNumber; + private int reason; + private Date revocationDate; +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/response/CMCResponseModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/response/CMCResponseModel.java new file mode 100644 index 0000000..f0e04a0 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/response/CMCResponseModel.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.response; + +import org.bouncycastle.asn1.cmc.BodyPartID; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; +import se.swedenconnect.ca.cmc.api.data.CMCResponseStatus; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for the CMC response model specifying data for the CMC response + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public interface CMCResponseModel { + + /** + * Gets the request nonce + * @return request nonce + */ + byte[] getNonce(); + + /** + * Gets the registration info data. Each request type identifies the syntax of this parameter + * @return registration info data + */ + byte[] getResponseInfo(); + + /** + * The status of the response + * @return cmc response status + */ + CMCResponseStatus getCmcResponseStatus(); + + /** + * Return certificates for the response + * @return list of certificate bytes + */ + List getReturnCertificates(); + + CMCRequestType getCmcRequestType(); +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/AbstractCMCResponseModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/AbstractCMCResponseModel.java new file mode 100644 index 0000000..0e9aff4 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/AbstractCMCResponseModel.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.response.impl; + +import lombok.Getter; +import lombok.Setter; +import org.bouncycastle.asn1.cmc.BodyPartID; +import se.swedenconnect.ca.cmc.api.data.CMCResponseStatus; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.response.CMCResponseModel; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract implementation of the CMC response model + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class AbstractCMCResponseModel implements CMCResponseModel { + + public AbstractCMCResponseModel(byte[] nonce, CMCResponseStatus cmcResponseStatus, CMCRequestType cmcRequestType) { + this(nonce, cmcResponseStatus, cmcRequestType, null); + } + + public AbstractCMCResponseModel(byte[] nonce, CMCResponseStatus cmcResponseStatus, CMCRequestType cmcRequestType, byte[] responseInfo) { + this.nonce = nonce; + this.responseInfo = responseInfo; + this.cmcResponseStatus = cmcResponseStatus; + this.returnCertificates = new ArrayList<>(); + this.cmcRequestType = cmcRequestType; + } + + @Getter @Setter protected byte[] nonce; + @Getter @Setter protected byte[] responseInfo; + @Getter @Setter protected List returnCertificates; + @Getter protected CMCResponseStatus cmcResponseStatus; + @Getter protected CMCRequestType cmcRequestType; +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/CMCAdminResponseModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/CMCAdminResponseModel.java new file mode 100644 index 0000000..78adc23 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/CMCAdminResponseModel.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.response.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Getter; +import org.bouncycastle.asn1.cmc.BodyPartID; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; +import se.swedenconnect.ca.cmc.api.data.CMCResponseStatus; +import se.swedenconnect.ca.cmc.api.data.CMCStatusType; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.io.IOException; +import java.util.List; + +/** + * Response model for creating CMC responses for Admin requests + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCAdminResponseModel extends AbstractCMCResponseModel { + + @Getter private AdminCMCData adminCMCData; + + public CMCAdminResponseModel(byte[] nonce, CMCResponseStatus cmcResponseStatus, CMCRequestType cmcRequestType, AdminCMCData adminCMCData) throws IOException { + super(nonce, cmcResponseStatus, cmcRequestType, getResponseInfo(adminCMCData)); + this.adminCMCData = adminCMCData; + } + + private static byte[] getResponseInfo(AdminCMCData adminCMCData) throws IOException { + try { + return CMCUtils.OBJECT_MAPPER.writeValueAsBytes(adminCMCData); + } + catch (JsonProcessingException e) { + throw new IOException("Unable to convert admin request data to JSON", e); + } + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/CMCBasicCMCResponseModel.java b/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/CMCBasicCMCResponseModel.java new file mode 100644 index 0000000..937dca3 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/model/response/impl/CMCBasicCMCResponseModel.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.model.response.impl; + +import lombok.Getter; +import lombok.Setter; +import org.bouncycastle.asn1.cmc.BodyPartID; +import org.bouncycastle.cert.X509CertificateHolder; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; +import se.swedenconnect.ca.cmc.api.data.CMCResponseStatus; +import se.swedenconnect.ca.cmc.api.data.CMCStatusType; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.engine.utils.CAUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Generic CMC response model for creating CMC responses + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCBasicCMCResponseModel extends AbstractCMCResponseModel { + + public CMCBasicCMCResponseModel(byte[] nonce, CMCResponseStatus cmcResponseStatus, CMCRequestType cmcRequestType, byte[] responseInfo) { + super(nonce, cmcResponseStatus, cmcRequestType, responseInfo); + } + + /** + * + * @param nonce + * @param cmcResponseStatus + * @param responseInfo + * @param returnCertificates + * @throws CertificateException + * @throws IOException + */ + public CMCBasicCMCResponseModel(byte[] nonce, CMCResponseStatus cmcResponseStatus, CMCRequestType cmcRequestType, byte[] responseInfo, List returnCertificates) + throws CertificateException, IOException { + super(nonce, cmcResponseStatus, cmcRequestType, responseInfo); + addCertificates(returnCertificates); + } + + private void addCertificates(List returnCertificates) throws CertificateException, IOException { + List certDataList = new ArrayList<>(); + for (Object o : returnCertificates){ + if (o instanceof X509Certificate) { + certDataList.add((X509Certificate)o); + continue; + } + if (o instanceof X509CertificateHolder) { + certDataList.add(CAUtils.getCert((X509CertificateHolder)o)); + continue; + } + throw new IOException("Illegal certificate type"); + } + setReturnCertificates(certDataList); + } + +} diff --git a/src/main/java/se/swedenconnect/ca/cmc/package-info.java b/src/main/java/se/swedenconnect/ca/cmc/package-info.java new file mode 100644 index 0000000..23a4722 --- /dev/null +++ b/src/main/java/se/swedenconnect/ca/cmc/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * CMC implementation + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +package se.swedenconnect.ca.cmc; \ No newline at end of file diff --git a/src/test/java/se/swedenconnect/ca/cmc/CMCTests.java b/src/test/java/se/swedenconnect/ca/cmc/CMCTests.java new file mode 100644 index 0000000..61f6c49 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/CMCTests.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc; + +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.cmc.BodyPartID; +import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers; +import org.bouncycastle.asn1.x509.CRLReason; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import se.idsec.sigval.cert.chain.ExtendedCertPathValidatorException; +import se.swedenconnect.ca.cmc.api.*; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.api.data.CMCResponse; +import se.swedenconnect.ca.cmc.api.data.CMCStatusType; +import se.swedenconnect.ca.cmc.api.impl.DefaultCMCCaApi; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.auth.impl.DefaultCMCReplayChecker; +import se.swedenconnect.ca.cmc.auth.impl.DefaultCMCValidator; +import se.swedenconnect.ca.cmc.ca.*; +import se.swedenconnect.ca.cmc.data.CMCRequestData; +import se.swedenconnect.ca.cmc.data.TestResponseStatus; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.AdminRequestType; +import se.swedenconnect.ca.cmc.model.admin.request.ListCerts; +import se.swedenconnect.ca.cmc.model.admin.response.CAInformation; +import se.swedenconnect.ca.cmc.model.admin.response.CertificateData; +import se.swedenconnect.ca.cmc.model.request.CMCRequestModel; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.request.impl.CMCAdminRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCCertificateRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCGetCertRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCRevokeRequestModel; +import se.swedenconnect.ca.cmc.model.response.CMCResponseModel; +import se.swedenconnect.ca.cmc.model.response.impl.CMCAdminResponseModel; +import se.swedenconnect.ca.cmc.model.response.impl.CMCBasicCMCResponseModel; +import se.swedenconnect.ca.cmc.utils.CMCDataPrint; +import se.swedenconnect.ca.cmc.utils.CMCDataValidator; +import se.swedenconnect.ca.cmc.utils.CMCSigner; +import se.swedenconnect.ca.cmc.utils.TestUtils; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.DefaultCertificateModelBuilder; +import se.swedenconnect.ca.engine.ca.repository.SortBy; +import se.swedenconnect.ca.engine.configuration.CAAlgorithmRegistry; +import se.swedenconnect.ca.engine.utils.CAUtils; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class CMCTests { + + private final static SecureRandom RNG = new SecureRandom(); + private static CMCSigner cmcSigner; + private static X509CertificateHolder testCert01; + private static X509CertificateHolder testCert02; + private static X509CertificateHolder testCert03; + + /** + * Initializes the CA services used to provide certificates and revocation services based on this library + */ + @BeforeAll + public static void init() { + Security.addProvider(new BouncyCastleProvider()); + log.info("Setting up test CA:s"); + TestServices.addCa(TestCA.INSTANCE1); + TestServices.addCa(TestCA.RA_CA); + TestServices.addCa(TestCA.ECDSA_CA); + TestServices.addValidators(true); + + try { + KeyPair raKeyPair = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + TestCAHolder raCaHolder = TestServices.getTestCAs().get(TestCA.RA_CA); + TestCAService raSignerCA = raCaHolder.getCscaService(); + CertificateModel certificateModel = raSignerCA.getCertificateModelBuilder( + CertRequestData.getTypicalServiceName("RA Signer", "XX"), raKeyPair.getPublic()).build(); + X509CertificateHolder raCert = raSignerCA.issueCertificate(certificateModel); + cmcSigner = new CMCSigner(raKeyPair, TestUtils.getCertificate(raCert.getEncoded())); + + TestCAHolder caHolder = TestServices.getTestCAs().get(TestCA.INSTANCE1); + TestCAService ca = caHolder.getCscaService(); + + KeyPair subjectKeyPair1 = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + DefaultCertificateModelBuilder subj1CertModelBuilder = ca.getCertificateModelBuilder( + CMCRequestData.subjectMap.get(CMCRequestData.USER1), subjectKeyPair1.getPublic()); + testCert01 = ca.issueCertificate(subj1CertModelBuilder.build()); + KeyPair subjectKeyPair2 = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + DefaultCertificateModelBuilder subj2CertModelBuilder = ca.getCertificateModelBuilder( + CMCRequestData.subjectMap.get(CMCRequestData.USER2), subjectKeyPair2.getPublic()); + testCert02 = ca.issueCertificate(subj2CertModelBuilder.build()); + KeyPair subjectKeyPair3 = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + DefaultCertificateModelBuilder subj3CertModelBuilder = ca.getCertificateModelBuilder( + CMCRequestData.subjectMap.get(CMCRequestData.USER3), subjectKeyPair3.getPublic()); + testCert03 = ca.issueCertificate(subj3CertModelBuilder.build()); + + } + catch (Exception e) { + e.printStackTrace(); + } + + } + + @Test + public void checkCMCRequest() throws Exception { + + TestCAHolder caHolder = TestServices.getTestCAs().get(TestCA.INSTANCE1); + TestCAService ca = caHolder.getCscaService(); + KeyPair subjectKeyPair = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + + DefaultCertificateModelBuilder certificateModelBuilder = ca.getCertificateModelBuilder( + CMCRequestData.subjectMap.get(CMCRequestData.USER1), subjectKeyPair.getPublic()); + CertificateModel certificateModel = certificateModelBuilder.build(); + + CMCRequestFactory cmcRequestFactory = new CMCRequestFactory(cmcSigner.getSignerChain(), cmcSigner.getContentSigner()); + CMCRequestParser cmcRequestParser = new CMCRequestParser(new DefaultCMCValidator(cmcSigner.getSignerChain().get(0)), + new DefaultCMCReplayChecker(60, 1)); + // Note that the replay checker time settings here does not make sense for production. Max age must always be shorter than retention time or else + // replay detection will fail as nonces are accepted for longer time than they are retained. These setting values are set to allow testing + // the replay checker clear cache capability without invalidating the tested nonce. + + CMCRequestModel requestModel; + CMCRequest cmcRequest; + CMCRequest cmcParsed; + //Create certificate request with PKCS#10 + + requestModel = getCMCRequest(certificateModel, subjectKeyPair, false); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Certificate request with PKCS#10:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed CMC Certificate request with PKCS#10:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Create certificate request with CRMF + requestModel = getCMCRequest(certificateModel, subjectKeyPair, true); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Certificate request with CRMF:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed CMC Certificate request with CRMF:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Create CMC revoke request + BigInteger certSerial = testCert01.getSerialNumber(); + requestModel = new CMCRevokeRequestModel(certSerial, CRLReason.unspecified, new Date(), ca.getCaCertificate().getSubject()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Revoke request:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed CMC Revoke request:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Create CMC Get Cert request + requestModel = new CMCGetCertRequestModel(certSerial, ca.getCaCertificate().getSubject()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Get Cert request:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed CMC Get Cert request:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Create CMC Admin request - CA Info + requestModel = new CMCAdminRequestModel(CMCRequestData.adminRequestMap.get(CMCRequestData.CA_INFO)); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Admin request - CA Info:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed Admin request - CA Info:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Create CMC Admin request - list certs + requestModel = new CMCAdminRequestModel(CMCRequestData.adminRequestMap.get(CMCRequestData.LIST_CERTS)); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Admin request - List Certs:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed Admin request - List Certs:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Create CMC Admin request - list all serials + requestModel = new CMCAdminRequestModel(CMCRequestData.adminRequestMap.get(CMCRequestData.LIST_CERT_SERIALS)); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC Admin request - List All Serials:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Parsed Admin request - List All Serials:\n{}", CMCDataPrint.printCMCRequest(cmcParsed, false, false)); + CMCDataValidator.validateCMCRequest(cmcParsed, requestModel); + + //Replay request + log.info("Replay test - Parsing old request"); + try { + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + throw new RuntimeException("This is a replay, but this was not detected"); + } catch (IOException ex){ + log.info("Replay detection succeeded: {}", ex.toString()); + // This time should allow the replay cache to be cleared as defined + Thread.sleep(1000); + cmcParsed = cmcRequestParser.parseCMCrequest(cmcRequest.getCmcRequestBytes()); + log.info("Replay cache successfully cleared"); + } + + } + + @Test + public void checkCMCResponses() throws Exception { + TestCAHolder caHolder = TestServices.getTestCAs().get(TestCA.INSTANCE1); + TestCAService ca = caHolder.getCscaService(); + PublicKey caPublicKey = CAUtils.getCert(ca.getCaCertificate()).getPublicKey(); + + CMCResponseFactory cmcResponseFactory = new CMCResponseFactory(cmcSigner.getSignerChain(), cmcSigner.getContentSigner()); + CMCResponseParser cmcResponseParser = new CMCResponseParser(new DefaultCMCValidator(cmcSigner.getSignerChain().get(0)), caPublicKey); + + CMCResponseModel responseModel; + CMCResponse cmcResponse; + CMCResponse cmcParsed; + CMCRequestType cmcRequestType; + + byte[] nonce = new byte[128]; + RNG.nextBytes(nonce); + List processedObjects = Arrays.asList( + new BodyPartID(Long.parseLong("1134")), + new BodyPartID(Long.parseLong("1234")), + new BodyPartID(Long.parseLong("345")) + ); + + cmcRequestType = CMCRequestType.issueCert; + responseModel = new CMCBasicCMCResponseModel(nonce, TestResponseStatus.success.withBodyParts(processedObjects), cmcRequestType, + "profile".getBytes(StandardCharsets.UTF_8), List.of(testCert01)); + cmcResponse = cmcResponseFactory.getCMCResponse(responseModel); + log.info("CMC Cert Issue Success response:\n{}", CMCDataPrint.printCMCResponse(cmcResponse, true)); + CMCDataValidator.validateCMCResponse(cmcResponse, responseModel); + cmcParsed = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), cmcRequestType); + log.info("Parsed CMC Cert Issue Success response:\n{}", CMCDataPrint.printCMCResponse(cmcParsed, false)); + CMCDataValidator.validateCMCResponse(cmcParsed, responseModel); + + cmcRequestType = CMCRequestType.issueCert; + responseModel = new CMCBasicCMCResponseModel(nonce, TestResponseStatus.failBadRequest.withBodyParts(processedObjects), cmcRequestType, + "profile".getBytes( + StandardCharsets.UTF_8), new ArrayList<>()); + cmcResponse = cmcResponseFactory.getCMCResponse(responseModel); + log.info("CMC Fail response:\n{}", CMCDataPrint.printCMCResponse(cmcResponse, true)); + CMCDataValidator.validateCMCResponse(cmcResponse, responseModel); + cmcParsed = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), cmcRequestType); + log.info("Parsed CMC Success response:\n{}", CMCDataPrint.printCMCResponse(cmcParsed, false)); + CMCDataValidator.validateCMCResponse(cmcParsed, responseModel); + + cmcRequestType = CMCRequestType.admin; + responseModel = new CMCAdminResponseModel(nonce, TestResponseStatus.success.withBodyParts(processedObjects), cmcRequestType, + AdminCMCData.builder() + .adminRequestType(AdminRequestType.caInfo) + .data(CMCUtils.OBJECT_MAPPER.writeValueAsString(CAInformation.builder() + .certificateChain(List.of(ca.getCaCertificate().getEncoded())) + .certificateCount(125) + .validCertificateCount(102) + .build())) + .build()); + cmcResponse = cmcResponseFactory.getCMCResponse(responseModel); + log.info("CMC CA info response:\n{}", CMCDataPrint.printCMCResponse(cmcResponse, true)); + CMCDataValidator.validateCMCResponse(cmcResponse, responseModel); + cmcParsed = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), cmcRequestType); + log.info("Parsed CA info response:\n{}", CMCDataPrint.printCMCResponse(cmcParsed, false)); + CMCDataValidator.validateCMCResponse(cmcParsed, responseModel); + + cmcRequestType = CMCRequestType.admin; + responseModel = new CMCAdminResponseModel(nonce, TestResponseStatus.success.withBodyParts(processedObjects), cmcRequestType, + AdminCMCData.builder() + .adminRequestType(AdminRequestType.listCerts) + .data(CMCUtils.OBJECT_MAPPER.writeValueAsString(Arrays.asList( + CertificateData.builder() + .certificate(testCert01.getEncoded()) + .revoked(true) + .build(), + CertificateData.builder() + .certificate(testCert02.getEncoded()) + .revoked(true) + .build(), + CertificateData.builder() + .certificate(testCert03.getEncoded()) + .revoked(false) + .revocationDate(System.currentTimeMillis()) + .revocationReason(0) + .build() + ))) + .build()); + cmcResponse = cmcResponseFactory.getCMCResponse(responseModel); + log.info("CMC List cert response:\n{}", CMCDataPrint.printCMCResponse(cmcResponse, true)); + CMCDataValidator.validateCMCResponse(cmcResponse, responseModel); + cmcParsed = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), cmcRequestType); + log.info("Parsed List cert response:\n{}", CMCDataPrint.printCMCResponse(cmcParsed, false)); + CMCDataValidator.validateCMCResponse(cmcParsed, responseModel); + + //List all serials + cmcRequestType = CMCRequestType.admin; + responseModel = new CMCAdminResponseModel(nonce, TestResponseStatus.success.withBodyParts(processedObjects), cmcRequestType, + AdminCMCData.builder() + .adminRequestType(AdminRequestType.allCertSerials) + .data(CMCUtils.OBJECT_MAPPER.writeValueAsString(Arrays.asList( + BigInteger.ONE, BigInteger.TWO, BigInteger.TEN + ))) + .build()); + cmcResponse = cmcResponseFactory.getCMCResponse(responseModel); + log.info("CMC List all serials response:\n{}", CMCDataPrint.printCMCResponse(cmcResponse, true)); + CMCDataValidator.validateCMCResponse(cmcResponse, responseModel); + cmcParsed = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), cmcRequestType); + log.info("Parsed List all serials response:\n{}", CMCDataPrint.printCMCResponse(cmcParsed, false)); + CMCDataValidator.validateCMCResponse(cmcParsed, responseModel); + } + + @Test + public void checkCMCCaApi() throws Exception { + + TestCAHolder caHolder = TestServices.getTestCAs().get(TestCA.INSTANCE1); + TestCAService ca = caHolder.getCscaService(); + PublicKey caPublicKey = CAUtils.getCert(ca.getCaCertificate()).getPublicKey(); + CMCResponseFactory cmcResponseFactory = new CMCResponseFactory(cmcSigner.getSignerChain(), cmcSigner.getContentSigner()); + CMCRequestParser cmcRequestParser = new CMCRequestParser(new DefaultCMCValidator(cmcSigner.getSignerChain().get(0)), + new DefaultCMCReplayChecker()); + CMCRequestFactory cmcRequestFactory = new CMCRequestFactory(cmcSigner.getSignerChain(), cmcSigner.getContentSigner()); + CMCResponseParser cmcResponseParser = new CMCResponseParser(new DefaultCMCValidator(cmcSigner.getSignerChain().get(0)), caPublicKey); + KeyPair subjectKeyPair = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + final CertValidatorComponents certValidator = TestServices.getValidator(ValidatorProfile.NORMAL, true); + + DefaultCertificateModelBuilder p10CertificateModelBuilder = ca.getCertificateModelBuilder( + CMCRequestData.subjectMap.get(CMCRequestData.PKCS10_USER), subjectKeyPair.getPublic()); + CertificateModel p10CertificateModel = p10CertificateModelBuilder.build(); + DefaultCertificateModelBuilder crmfCertificateModelBuilder = ca.getCertificateModelBuilder( + CMCRequestData.subjectMap.get(CMCRequestData.CRMF_USER), subjectKeyPair.getPublic()); + CertificateModel crmfCertificateModel = crmfCertificateModelBuilder.build(); + + CMCCaApi cmcCaApi = new DefaultCMCCaApi(ca, cmcRequestParser, cmcResponseFactory); + + CMCRequest cmcRequest; + CMCRequestModel requestModel; + CMCResponse cmcResponse; + CMCResponse parsedCMCResponse; + + // Issue cert with PKCS#10 + requestModel = getCMCRequest(p10CertificateModel, subjectKeyPair, false); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API Certificate request with PKCS#10:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API Certificate request with PKCS#10:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + Assertions.assertEquals(cmcResponse.getResponseStatus().getStatus(), CMCStatusType.success); + Assertions.assertEquals(1, cmcResponse.getReturnCertificates().size()); + Assertions.assertEquals(parsedCMCResponse.getResponseStatus().getStatus(), CMCStatusType.success); + Assertions.assertEquals(1, parsedCMCResponse.getReturnCertificates().size()); + final X509Certificate p10Cert = cmcResponse.getReturnCertificates().get(0); + + // Issue Cert with CRMF + requestModel = getCMCRequest(crmfCertificateModel, subjectKeyPair, true); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API Certificate request with CRMF:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API Certificate request with CRMF:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + Assertions.assertEquals(cmcResponse.getResponseStatus().getStatus(), CMCStatusType.success); + Assertions.assertEquals(1, cmcResponse.getReturnCertificates().size()); + Assertions.assertEquals(parsedCMCResponse.getResponseStatus().getStatus(), CMCStatusType.success); + Assertions.assertEquals(1, parsedCMCResponse.getReturnCertificates().size()); + final X509Certificate crmfCert = cmcResponse.getReturnCertificates().get(0); + + // Revoke certificate + requestModel = new CMCRevokeRequestModel(crmfCert.getSerialNumber(), CRLReason.keyCompromise, new Date(), + ca.getCaCertificate().getIssuer()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API Certificate Revocation:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + checkCertStatus(certValidator, crmfCert, true); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API Certificate Revocation:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + checkCertStatus(certValidator, crmfCert, false); + + // Revoke non-existant cert with serial number 1 + requestModel = new CMCRevokeRequestModel(BigInteger.ONE, CRLReason.unspecified, new Date(), ca.getCaCertificate().getIssuer()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API Certificate Revocation of unknown serial number:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, false)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API Certificate Revocation of unknown serial number:\n{}", + CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + Assertions.assertEquals(cmcResponse.getResponseStatus().getStatus(), CMCStatusType.failed); + Assertions.assertEquals(cmcResponse.getResponseStatus().getFailType(), CMCFailType.badCertId); + Assertions.assertEquals(parsedCMCResponse.getResponseStatus().getStatus(), CMCStatusType.failed); + Assertions.assertEquals(parsedCMCResponse.getResponseStatus().getFailType(), CMCFailType.badCertId); + + // Get Cert + requestModel = new CMCGetCertRequestModel(crmfCert.getSerialNumber(), ca.getCaCertificate().getSubject()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API Get Certificate:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + X509Certificate getCertCertificate = cmcResponse.getReturnCertificates().get(0); + Assertions.assertEquals(crmfCert, getCertCertificate); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API Get Certificate:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + Assertions.assertEquals(cmcResponse.getResponseStatus().getStatus(), CMCStatusType.success); + Assertions.assertEquals(1, cmcResponse.getReturnCertificates().size()); + Assertions.assertEquals(crmfCert, cmcResponse.getReturnCertificates().get(0)); + Assertions.assertEquals(parsedCMCResponse.getResponseStatus().getStatus(), CMCStatusType.success); + Assertions.assertEquals(1, parsedCMCResponse.getReturnCertificates().size()); + Assertions.assertEquals(crmfCert, parsedCMCResponse.getReturnCertificates().get(0)); + + // CAInfo + requestModel = new CMCAdminRequestModel(AdminCMCData.builder().adminRequestType(AdminRequestType.caInfo).build()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API CA Info:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API Get Certificate:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + CAInformation caInformation = CMCUtils.getCAInformation(cmcResponse); + Assertions.assertEquals(5, caInformation.getCertificateCount()); + Assertions.assertEquals(4, caInformation.getValidCertificateCount()); + Assertions.assertArrayEquals(ca.getOCSPResponderCertificate().getEncoded(), caInformation.getOcspCertificate()); + Assertions.assertEquals(1, caInformation.getCertificateChain().size()); + Assertions.assertArrayEquals(ca.getCaCertificate().getEncoded(), caInformation.getCertificateChain().get(0)); + + // List cert serials + requestModel = new CMCAdminRequestModel(AdminCMCData.builder().adminRequestType(AdminRequestType.allCertSerials).build()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API List cert serials:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API List cert serials:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + final List allSerials = CMCUtils.getAllSerials(cmcResponse); + Assertions.assertEquals(5, allSerials.size()); + Assertions.assertTrue(allSerials.contains(testCert01.getSerialNumber())); + Assertions.assertTrue(allSerials.contains(testCert02.getSerialNumber())); + Assertions.assertTrue(allSerials.contains(testCert03.getSerialNumber())); + Assertions.assertTrue(allSerials.contains(p10Cert.getSerialNumber())); + Assertions.assertTrue(allSerials.contains(crmfCert.getSerialNumber())); + + //Get all certs + AdminCMCData adminData = (AdminCMCData) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_responseInfo, + cmcResponse.getPkiResponse()).getValue(); + List serialHexStrList = CMCUtils.OBJECT_MAPPER.readValue(adminData.getData(), new TypeReference<>() { + }); + log.info("Getting all certificates for all serial numbers {}", String.join(", ", serialHexStrList)); + for (BigInteger certSerial : allSerials) { + requestModel = new CMCGetCertRequestModel(certSerial, ca.getCaCertificate().getSubject()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + X509Certificate cert = cmcResponse.getReturnCertificates().get(0); + log.info("Certificate issued to: {}", cert.getSubjectX500Principal()); + try { + certValidator.getCertificateValidator().validate(cert, null); + log.info("Certificate is valid"); + Assertions.assertNotEquals(crmfCert.getSerialNumber(), certSerial); + } + catch (Exception ex) { + log.info("Certificate is invalid/revoked"); + Assertions.assertEquals(crmfCert.getSerialNumber(), certSerial); + } + } + + // List certs + requestModel = new CMCAdminRequestModel(AdminCMCData.builder().adminRequestType(AdminRequestType.listCerts) + .data(CMCUtils.OBJECT_MAPPER.writeValueAsString(ListCerts.builder() + .pageIndex(1) + .pageSize(3) + .notRevoked(true) + .sortBy(SortBy.issueDate) + .build())) + .build()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API List 2nd page valid certs:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API List 2nd page valid certs:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + List certList = CMCUtils.getCertList(cmcResponse); + Assertions.assertEquals(1, certList.size()); + Assertions.assertArrayEquals(p10Cert.getEncoded(), certList.get(0).getCertificate()); + Assertions.assertFalse(certList.get(0).isRevoked()); + + // List certs + requestModel = new CMCAdminRequestModel(AdminCMCData.builder().adminRequestType(AdminRequestType.listCerts) + .data(CMCUtils.OBJECT_MAPPER.writeValueAsString(ListCerts.builder() + .pageIndex(1) + .pageSize(3) + .notRevoked(false) + .sortBy(SortBy.issueDate) + .build())) + .build()); + cmcRequest = cmcRequestFactory.getCMCRequest(requestModel); + log.info("CMC API List 2nd page all certs:\n{}", CMCDataPrint.printCMCRequest(cmcRequest, true, true)); + CMCDataValidator.validateCMCRequest(cmcRequest, requestModel); + cmcResponse = cmcCaApi.processRequest(cmcRequest.getCmcRequestBytes()); + parsedCMCResponse = cmcResponseParser.parseCMCresponse(cmcResponse.getCmcResponseBytes(), requestModel.getCmcRequestType()); + log.info("CMC response from API List 2nd page valid certs:\n{}", CMCDataPrint.printCMCResponse(parsedCMCResponse, true)); + certList = CMCUtils.getCertList(cmcResponse); + Assertions.assertEquals(2, certList.size()); + Assertions.assertArrayEquals(crmfCert.getEncoded(), certList.get(1).getCertificate()); + Assertions.assertTrue(certList.get(1).isRevoked()); + + } + + private void checkCertStatus(CertValidatorComponents certValidator, X509Certificate targetCert, boolean expValid) throws Exception { + log.info("Validating certificate: {}", targetCert.getSubjectX500Principal()); + try { + certValidator.getCertificateValidator().validate(targetCert, null); + log.info("Certificate was valid"); + if (!expValid) { + throw new IOException("Certificate was expected to be revoked, but was valid"); + } + } + catch (ExtendedCertPathValidatorException ex) { + log.info("Certificate validation error: {}", ex.getMessage()); + if (expValid) { + throw new IOException("Certificate was expected to be valid, but was revoked"); + } + } + } + + private CMCRequestModel getCMCRequest(CertificateModel certificateModel, KeyPair kp, boolean crmf) { + + return crmf + ? new CMCCertificateRequestModel(certificateModel, "profileCrmf") + : new CMCCertificateRequestModel(certificateModel, "profilePkcs10", + kp.getPrivate(), CAAlgorithmRegistry.ALGO_ID_SIGNATURE_ECDSA_SHA256); + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/CertRequestData.java b/src/test/java/se/swedenconnect/ca/cmc/ca/CertRequestData.java new file mode 100644 index 0000000..29cbfd3 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/CertRequestData.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.cert.X509CertificateHolder; +import se.idsec.x509cert.extensions.InhibitAnyPolicy; +import se.idsec.x509cert.extensions.PrivateKeyUsagePeriod; +import se.idsec.x509cert.extensions.QCStatements; +import se.idsec.x509cert.extensions.data.MonetaryValue; +import se.idsec.x509cert.extensions.data.PDSLocation; +import se.idsec.x509cert.extensions.data.SemanticsInformation; +import se.swedenconnect.ca.engine.ca.attribute.CertAttributes; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuerModel; +import se.swedenconnect.ca.engine.ca.models.cert.AttributeModel; +import se.swedenconnect.ca.engine.ca.models.cert.AttributeTypeAndValueModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.EntityType; +import se.swedenconnect.ca.engine.ca.models.cert.extension.ExtensionModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.data.*; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.CertificatePolicyModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.GenericExtensionModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.SubjDirectoryAttributesModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.AlternativeNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.BasicConstraintsModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.ExtendedKeyUsageModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.KeyUsageModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.DefaultCertificateModelBuilder; +import se.swedenconnect.ca.engine.ca.models.cert.impl.ExplicitCertNameModel; + +import java.math.BigInteger; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Generating basic certificate request data for test + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CertRequestData { + + public static CertNameModel getCompleteSubjectName() { + CertNameModel subjectName = new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.C) + .value("SE").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.O) + .value("Organization AB").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.OU) + .value("Dev department").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.SERIALNUMBER) + .value("196405065683").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.GIVENNAME) + .value("Nisse").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.SURNAME) + .value("Hult").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.CN) + .value("Nisse Hult").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.T) + .value("CEO").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.EmailAddress) + .value("nisse.hult@example.com").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.DATE_OF_BIRTH) + .value("1964-05-06").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.ORGANIZATION_IDENTIFIER) + .value("556778-1122").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.DC) + .value("example.com").build() + )); + return subjectName; + } + + public static CertNameModel getTypicalSubejctName(String givenName, String surname, String id) { + CertNameModel subjectName = new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.C) + .value("SE").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.SERIALNUMBER) + .value(id).build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.GIVENNAME) + .value(givenName).build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.SURNAME) + .value(surname).build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.CN) + .value(givenName + " " + surname).build() + )); + return subjectName; + } + + public static CertNameModel getTypicalServiceName(String commonName, String country) { + CertNameModel subjectName = new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.C) + .value(country).build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.O) + .value("Organization AB").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.OU) + .value("Service department").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.ORGANIZATION_IDENTIFIER) + .value("556677-1122").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.CN) + .value(commonName).build() + )); + return subjectName; + } + + public static DefaultCertificateModelBuilder getCompleteCertModelBuilder(PublicKey publicKey, X509CertificateHolder issuerCert, + CertificateIssuerModel issuerModel) { + DefaultCertificateModelBuilder builder = DefaultCertificateModelBuilder.getInstance(publicKey, issuerCert, issuerModel); + builder + .subject(getCompleteSubjectName()) + .basicConstraints(new BasicConstraintsModel(3, true)) + .includeAki(true) + .includeSki(true) + .keyUsage(new KeyUsageModel(KeyUsage.keyCertSign + KeyUsage.cRLSign + KeyUsage.digitalSignature, true)) + .extendedKeyUsage(new ExtendedKeyUsageModel(true, KeyPurposeId.id_kp_OCSPSigning)) + .crlDistributionPoints(Arrays.asList("http://example.com/crl1", "http://example.com/crl2")) + .ocspServiceUrl("http://example.com/ocsp") + .issuerCertUrl("http://example.com/issuerCert") + .certificatePolicy(new CertificatePolicyModel(true, Arrays.asList( + CertificatePolicyModel.PolicyInfoParams.builder() + .policy(new ASN1ObjectIdentifier("1.2.3.4.5")) + .cpsUri("https://example.com/cps") + .displayText("Detta är en display text ") + .build(), + CertificatePolicyModel.PolicyInfoParams.builder() + .policy(new ASN1ObjectIdentifier("1.2.3.4.6")) + .cpsUri("https://example.com/cps2") + .displayText("Detta är en annan display text ") + .build() + ))) + .authenticationContext(SAMLAuthContextBuilder.instance() + .assertionRef("091283098123098123") + .authenticationInstant(new Date()) + .authnContextClassRef("https://example.com/loa/loa3") + .identityProvider("https://example.com/idp") + .serviceID("http://example.com/service-provider") + .attributeMappings(Arrays.asList( + AttributeMappingBuilder.instance() + .name("name1").friendlyName("fName1").nameFormat("nameFormat") + .ref("2.5.4.1").type(AttributeRefType.rdn) + .build(), + AttributeMappingBuilder.instance() + .name("name2").friendlyName("fName2").nameFormat("nameFormat") + .ref("2.5.4.2").type(AttributeRefType.rdn) + .build(), + AttributeMappingBuilder.instance() + .name("name3").friendlyName("fName3").nameFormat("nameFormat") + .ref("6").type(AttributeRefType.san) + .build() + )) + .build()) + .caRepositoryUrl("http://example.com/certs") + .timeStampAuthorityUrl("http://example.com/timestamps") + .qcStatements(QcStatementsBuilder.instance() + .versionAndSemantics(new QCPKIXSyntax(new SemanticsInformation(QCStatements.ETSI_SEMANTICS_EIDAS_NATURAL, Arrays.asList( + new GeneralName(GeneralName.uniformResourceIdentifier, "http://example.com/name-reg-authority-01"), + new GeneralName(GeneralName.uniformResourceIdentifier, "http://example.com/name-reg-authority-02"))))) + .qualifiedCertificate(true) + .qscd(true) + .qcTypes(Arrays.asList(QCStatements.QC_TYPE_ELECTRONIC_SIGNATURE)) + .legislationCountries(Arrays.asList("SE", "NO")) + .relianceLimit(new MonetaryValue("SEK", new BigInteger("1"), new BigInteger("3"))) + .retentionPeriod(10) + .pdsLocations(Arrays.asList( + new PDSLocation("sv", "https://example.com/pds-location-sv"), + new PDSLocation("en", "https://example.com/pds-location-en"))) + .build()) + .subjectAltNames(Collections.singletonMap(GeneralName.uniformResourceIdentifier, "https://example.com/alt-name-uri")) + .subjectDirectoryAttributes(new SubjDirectoryAttributesModel(Arrays.asList( + new AttributeModel(CertAttributes.POSTAL_ADDRESS, "Scheelevägen 12", "223 70 Lund"), + new AttributeModel(CertAttributes.DATE_OF_BIRTH, "1962-11-02"), + new AttributeModel(CertAttributes.PLACE_OF_BIRTH, "Malmö"), + new AttributeModel(CertAttributes.GENDER, "M"), + new AttributeModel(CertAttributes.COUNTRY_OF_CITIZENSHIP, "SE", "DE") + ))) + .ocspNocheck(true); + return builder; + + } + + public static void addUncommonExtensions(CertificateModel certificateModel) { + // Add uncommon extensions + // IssuerAlternativeName + List extensionModels = certificateModel.getExtensionModels(); + extensionModels.add(new AlternativeNameModel(EntityType.issuer, + new GeneralName(GeneralName.uniformResourceIdentifier, "https://example.com/alt-name-uri"))); + + // InhibitAnyPolicy (Must be critical) + extensionModels.add(new GenericExtensionModel( + Extension.inhibitAnyPolicy, new InhibitAnyPolicy(1), true + )); + + // NameConstraints (MUST be critical) + extensionModels.add(new GenericExtensionModel( + Extension.nameConstraints, new NameConstraints( + new GeneralSubtree[] { + new GeneralSubtree(new GeneralName(GeneralName.uniformResourceIdentifier, "example.com")) + }, + new GeneralSubtree[] { + new GeneralSubtree(new GeneralName(1, "example.com")) + }), true + )); + + // PolicyConstraints (Must be critical) + extensionModels.add(new GenericExtensionModel( + Extension.policyConstraints, new PolicyConstraints(new BigInteger("1"), new BigInteger("1")), true + )); + // PolicyMappings + extensionModels.add(new GenericExtensionModel( + Extension.policyMappings, new PolicyMappings( + new CertPolicyId[] { CertPolicyId.getInstance(new ASN1ObjectIdentifier("1.2.3.4.5.6")), + CertPolicyId.getInstance(new ASN1ObjectIdentifier("1.2.3.4.5.7")) }, + new CertPolicyId[] { CertPolicyId.getInstance(new ASN1ObjectIdentifier("2.2.3.4.5.6")), + CertPolicyId.getInstance(new ASN1ObjectIdentifier("2.2.3.4.5.7")) } + ), true)); + + // PrivateKeyUsagePeriod + extensionModels.add(new GenericExtensionModel( + Extension.privateKeyUsagePeriod, new PrivateKeyUsagePeriod(new Date(), new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5)) + )); + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/CertValidatorComponents.java b/src/test/java/se/swedenconnect/ca/cmc/ca/CertValidatorComponents.java new file mode 100644 index 0000000..e8538b6 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/CertValidatorComponents.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.AllArgsConstructor; +import lombok.Data; +import se.idsec.sigval.cert.chain.impl.StatusCheckingCertificateValidatorImpl; +import se.idsec.sigval.cert.validity.crl.CRLCache; + +/** + * Components of a certificate validator used for test + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Data +@AllArgsConstructor +public class CertValidatorComponents { + StatusCheckingCertificateValidatorImpl certificateValidator; + CRLCache crlCache; +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/TestCA.java b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCA.java new file mode 100644 index 0000000..43a35ec --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCA.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import se.swedenconnect.ca.engine.configuration.CAAlgorithmRegistry; + +import java.security.KeyPair; + +/** + * Enumeration of configuration data for test CA providers + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@AllArgsConstructor +@Getter +public enum TestCA { + INSTANCE1( + "rsa-ca", + "XA", + "XA RSA Test CA", + TestServices.rsa2048kp01, + CAAlgorithmRegistry.ALGO_ID_SIGNATURE_RSA_SHA256, + "XA RSA OCSP responder", + TestServices.rsa2048kp02, + CAAlgorithmRegistry.ALGO_ID_SIGNATURE_RSA_SHA256), + + RA_CA( + "rsa-pss-ca", + "XB", + "XB RSA PSS Test CA", + TestServices.rsa2048kp01, + CAAlgorithmRegistry.ALGO_ID_SIGNATURE_RSA_SHA256_MGF1, + "XB RSA PSS OCSP responder", + TestServices.rsa2048kp02, + CAAlgorithmRegistry.ALGO_ID_SIGNATURE_RSA_SHA256), + ECDSA_CA( + "ecdsa-ca", + "XC", + "XC ECDSA Test CA", + TestServices.ec256kp01, + CAAlgorithmRegistry.ALGO_ID_SIGNATURE_ECDSA_SHA256, + "XC ECDSA OCSP responder", + TestServices.ec256kp02, + CAAlgorithmRegistry.ALGO_ID_SIGNATURE_ECDSA_SHA256); + + String id; + String country; + String caName; + KeyPair caKeyPair; + String caAlgo; + String ocspName; + KeyPair ocspKeyPair; + String ocspAlgo; + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/TestCAHolder.java b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCAHolder.java new file mode 100644 index 0000000..02728f7 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCAHolder.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import se.swedenconnect.ca.engine.ca.attribute.CertAttributes; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuer; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuerModel; +import se.swedenconnect.ca.engine.ca.issuer.impl.BasicCertificateIssuer; +import se.swedenconnect.ca.engine.ca.issuer.impl.SelfIssuedCertificateIssuer; +import se.swedenconnect.ca.engine.ca.models.cert.AttributeTypeAndValueModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertificateModelBuilder; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.CertificatePolicyModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.BasicConstraintsModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.ExtendedKeyUsageModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.KeyUsageModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.DefaultCertificateModelBuilder; +import se.swedenconnect.ca.engine.ca.models.cert.impl.ExplicitCertNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.SelfIssuedCertificateModelBuilder; +import se.swedenconnect.ca.engine.revocation.ocsp.OCSPModel; +import se.swedenconnect.ca.engine.revocation.ocsp.OCSPResponder; +import se.swedenconnect.ca.engine.revocation.ocsp.impl.RepositoryBasedOCSPResponder; + +import java.io.File; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.List; + +/** + * This class when instantiated creates a test CA services and related revocation services for CRL adn OCSP revocation checking. + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class TestCAHolder { + + public static final String FILE_URL_PREFIX = "http://file.example.com/"; + + private final File dataDir; + @Getter private TestCAService cscaService; + @Getter public final TestCA caConfig; + + /** + * Constructor for creating an instance of a test CSCA service + * @param caConfig Configuration parameters from the CSCA service + */ + public TestCAHolder(TestCA caConfig) { + this.dataDir = new File(System.getProperty("user.dir"), "target/test/ca-repo"); + this.caConfig = caConfig; + try { + setupCAs(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void setupCAs() throws Exception { + log.info("Setting up test CA {}", caConfig.getId()); + cscaService = createCSCAService(); + addOCSPResponder(); + } + + private TestCAService createCSCAService() throws Exception { + // generate key and root CA cert + CertificateIssuer certificateIssuer = new SelfIssuedCertificateIssuer(new CertificateIssuerModel( + caConfig.getCaAlgo(), + 20 + )); + + log.info("Generating root ca key for {}", caConfig.getId()); + KeyPair kp = caConfig.getCaKeyPair(); + CertNameModel name = getCAName(caConfig.getCaName()); + + CertificateModelBuilder builder = SelfIssuedCertificateModelBuilder.getInstance(kp, certificateIssuer.getCertificateIssuerModel()) + .subject(name) + .basicConstraints(new BasicConstraintsModel(true, true)) + .includeSki(true) + .keyUsage(new KeyUsageModel(KeyUsage.keyCertSign + KeyUsage.cRLSign, true)) + .certificatePolicy(new CertificatePolicyModel(true)); + X509CertificateHolder rootCA01Cert = certificateIssuer.issueCertificate(builder.build()); + File crlFile = new File(dataDir, caConfig.getId() + "/root-ca.crl"); + + return new TestCAService(kp.getPrivate(), Arrays.asList(rootCA01Cert), new TestCARepository(crlFile), crlFile, caConfig.getCaAlgo()); + } + + private CertNameModel getCAName(String commonName) { + return new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.C) + .value(caConfig.getCountry()).build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.O) + .value("Test Org").build(), + AttributeTypeAndValueModel.builder() + .attributeType(CertAttributes.CN) + .value(commonName).build() + )); + } + + private void addOCSPResponder() { + try { + log.info("Generating ocsp responder key for {}", caConfig.getId()); + + KeyPair kp; + String algorithm; + List ocspServiceChain; + if (caConfig.getOcspKeyPair() != null) { + // There is a dedicated key for OCSP responses. Setup an authorized responder + kp = caConfig.getOcspKeyPair(); + algorithm = caConfig.getOcspAlgo(); + DefaultCertificateModelBuilder certModelBuilder = cscaService.getCertificateModelBuilder( + CertRequestData.getTypicalServiceName(caConfig.getOcspName(), caConfig.getCountry()), kp.getPublic()); + + certModelBuilder + .qcStatements(null) + .keyUsage(new KeyUsageModel(KeyUsage.digitalSignature)) + .crlDistributionPoints(null) + .ocspServiceUrl(null) + .ocspNocheck(true) + .extendedKeyUsage(new ExtendedKeyUsageModel(true, KeyPurposeId.id_kp_OCSPSigning)); + + + X509CertificateHolder ocspIssuerCert = cscaService.getCertificateIssuer().issueCertificate(certModelBuilder.build()); + ocspServiceChain = Arrays.asList( + ocspIssuerCert, + cscaService.getCaCertificate() + ); + + } + else { + // We are issuing OCSP response directly from CA + kp = caConfig.getCaKeyPair(); + algorithm = caConfig.getCaAlgo(); + ocspServiceChain = Arrays.asList( + cscaService.getCaCertificate()); + } + + OCSPModel ocspModel = new OCSPModel(ocspServiceChain, cscaService.getCaCertificate(), algorithm); + OCSPResponder ocspResponder = new RepositoryBasedOCSPResponder(kp.getPrivate(), ocspModel, cscaService.getCaRepository()); + cscaService.setOcspResponder(ocspResponder, "https://example.com/" + caConfig.getId() + "/ocsp", ocspServiceChain.get(0)); + } + catch (Exception ex) { + log.error("Error creating OCSP responder", ex); + } + } + + public static String getFileUrl(File file) { + return getFileUrl(file.getAbsolutePath()); + } + + public static String getFileUrl(String path) { + String urlEncodedPath = URLEncoder.encode(path, StandardCharsets.UTF_8); + return FILE_URL_PREFIX + urlEncodedPath; + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/TestCARepository.java b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCARepository.java new file mode 100644 index 0000000..31c2650 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCARepository.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.bouncycastle.cert.X509CRLHolder; +import org.bouncycastle.cert.X509CertificateHolder; +import se.swedenconnect.ca.engine.ca.repository.CARepository; +import se.swedenconnect.ca.engine.ca.repository.CertificateRecord; +import se.swedenconnect.ca.engine.ca.repository.SortBy; +import se.swedenconnect.ca.engine.ca.repository.impl.SerializableCertificateRecord; +import se.swedenconnect.ca.engine.revocation.CertificateRevocationException; +import se.swedenconnect.ca.engine.revocation.crl.CRLRevocationDataProvider; +import se.swedenconnect.ca.engine.revocation.crl.RevokedCertificate; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Test implementation of a CA repository + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class TestCARepository implements CARepository, CRLRevocationDataProvider { + + private final File crlFile; + private List issuedCerts; + private BigInteger crlNumber; + + public TestCARepository(File crlFile) { + this.crlFile = crlFile; + this.issuedCerts = new ArrayList<>(); + this.crlNumber = BigInteger.ZERO; + } + + @Override public List getAllCertificates() { + return issuedCerts.stream() + .map(certificateRecord -> certificateRecord.getSerialNumber()) + .collect(Collectors.toList()); + } + + @Override public CertificateRecord getCertificate(BigInteger bigInteger) { + Optional recordOptional = issuedCerts.stream() + .filter(certificateRecord -> certificateRecord.getSerialNumber().equals(bigInteger)) + .findFirst(); + return recordOptional.isPresent() ? recordOptional.get() : null; + } + + @Override public void addCertificate(X509CertificateHolder certificate) throws IOException { + CertificateRecord record = getCertificate(certificate.getSerialNumber()); + if (record != null) { + throw new IOException("This certificate already exists in the certificate repository"); + } + issuedCerts.add(new SerializableCertificateRecord(certificate.getEncoded(), certificate.getSerialNumber(), + certificate.getNotBefore(), certificate.getNotAfter(), false, null, null)); + } + + @Override public void revokeCertificate(BigInteger serialNumber, int reason, Date revocationTime) throws CertificateRevocationException { + if (serialNumber == null) { + throw new CertificateRevocationException("Null Serial number"); + } + CertificateRecord certificateRecord = getCertificate(serialNumber); + if (certificateRecord == null) { + throw new CertificateRevocationException("No such certificate (" + serialNumber.toString(16) + ")"); + } + certificateRecord.setRevoked(true); + certificateRecord.setReason(reason); + certificateRecord.setRevocationTime(revocationTime); + } + + @Override public CRLRevocationDataProvider getCRLRevocationDataProvider() { + return this; + } + + @Override public List getRevokedCertificates() { + return issuedCerts.stream() + .filter(certificateRecord -> certificateRecord.isRevoked()) + .map(certificateRecord -> new RevokedCertificate( + certificateRecord.getSerialNumber(), + certificateRecord.getRevocationTime(), + certificateRecord.getReason() + )) + .collect(Collectors.toList()); + } + + @Override public BigInteger getNextCrlNumber() { + crlNumber = crlNumber.add(BigInteger.ONE); + return crlNumber; + } + + @Override public int getCertificateCount(boolean valid) { + if (!valid) { + return issuedCerts.size(); + } + + return (int) issuedCerts.stream() + .filter(certificateRecord -> !certificateRecord.isRevoked()) + .count(); + } + + @Override public List getCertificateRange(int page, int pageSize, boolean valid, SortBy sortBy) { + + List records = issuedCerts.stream() + .filter(certificateRecord -> { + if (valid) { + return !certificateRecord.isRevoked(); + } + return true; + }) + .collect(Collectors.toList()); + + if (sortBy != null) { + switch (sortBy) { + case serialNumber: + Collections.sort(records, Comparator.comparing(CertificateRecord::getSerialNumber)); + break; + case issueDate: + Collections.sort(records, Comparator.comparing(CertificateRecord::getIssueDate)); + break; + } + } + + int startIdx = page * pageSize; + int endIdx = startIdx + pageSize; + + if (startIdx > records.size()){ + return new ArrayList<>(); + } + + if (endIdx > records.size()) { + endIdx = records.size(); + } + + List resultCertList = new ArrayList<>(); + for (int i = startIdx; i removeExpiredCerts(int gracePeriodSeconds) { + List removedSerialList = new ArrayList<>(); + Date notBefore = new Date(System.currentTimeMillis() - (1000 * gracePeriodSeconds)); + issuedCerts = issuedCerts.stream() + .filter(certificateRecord -> { + final Date expiryDate = certificateRecord.getExpiryDate(); + // Check if certificate expired before the current time minus grace period + if (expiryDate.before(notBefore)){ + // Yes - Remove certificate + removedSerialList.add(certificateRecord.getSerialNumber()); + return false; + } + // No - keep certificate on repository + return true; + }) + .collect(Collectors.toList()); + return removedSerialList; + } + + + @SneakyThrows @Override public void publishNewCrl(X509CRLHolder crl) { + FileUtils.writeByteArrayToFile(crlFile, crl.getEncoded()); + } + + @SneakyThrows @Override public X509CRLHolder getCurrentCrl() { + return new X509CRLHolder(new FileInputStream(crlFile)); + } +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/TestCAService.java b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCAService.java new file mode 100644 index 0000000..95fcbc8 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/TestCAService.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.Getter; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuanceException; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuer; +import se.swedenconnect.ca.engine.ca.issuer.CertificateIssuerModel; +import se.swedenconnect.ca.engine.ca.issuer.impl.AbstractCAService; +import se.swedenconnect.ca.engine.ca.issuer.impl.BasicCertificateIssuer; +import se.swedenconnect.ca.engine.ca.models.cert.CertNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.data.AttributeMappingBuilder; +import se.swedenconnect.ca.engine.ca.models.cert.extension.data.AttributeRefType; +import se.swedenconnect.ca.engine.ca.models.cert.extension.data.SAMLAuthContextBuilder; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.BasicConstraintsModel; +import se.swedenconnect.ca.engine.ca.models.cert.extension.impl.simple.KeyUsageModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.DefaultCertificateModelBuilder; +import se.swedenconnect.ca.engine.ca.repository.CARepository; +import se.swedenconnect.ca.engine.revocation.CertificateRevocationException; +import se.swedenconnect.ca.engine.revocation.crl.CRLIssuer; +import se.swedenconnect.ca.engine.revocation.crl.CRLIssuerModel; +import se.swedenconnect.ca.engine.revocation.crl.CRLRevocationDataProvider; +import se.swedenconnect.ca.engine.revocation.crl.impl.DefaultCRLIssuer; +import se.swedenconnect.ca.engine.revocation.ocsp.OCSPResponder; + +import java.io.File; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * CA service for test + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class TestCAService extends AbstractCAService { + + private final File crlFile; + @Getter private CertificateIssuer certificateIssuer; + private CRLIssuer crlIssuer; + private List crlDistributionPoints; + private OCSPResponder ocspResponder; + private X509CertificateHolder ocspResponderCertificate; + @Getter private String ocspResponderUrl; + + public TestCAService(PrivateKey privateKey, List caCertificateChain, CARepository caRepository, + File crlFile, String algorithm) throws Exception { + super(caCertificateChain, caRepository); + this.crlFile = crlFile; + this.certificateIssuer = new BasicCertificateIssuer( + new CertificateIssuerModel(algorithm, 10), getCaCertificate().getSubject(), privateKey); + CRLIssuerModel crlIssuerModel = getCrlIssuerModel(getCaRepository().getCRLRevocationDataProvider(), algorithm); + this.crlDistributionPoints = new ArrayList<>(); + if (crlIssuerModel != null) { + this.crlIssuer = new DefaultCRLIssuer(crlIssuerModel, privateKey); + this.crlDistributionPoints = Arrays.asList(crlIssuerModel.getDistributionPointUrl()); + publishNewCrl(); + } + } + + private CRLIssuerModel getCrlIssuerModel(CRLRevocationDataProvider crlRevocationDataProvider, String algorithm) + throws CertificateRevocationException { + try { + return new CRLIssuerModel(getCaCertificate(), algorithm, + 2, crlRevocationDataProvider, TestCAHolder.getFileUrl(crlFile)); + } + catch (Exception e) { + throw new CertificateRevocationException(e); + } + } + + @Override public CertificateIssuer getCertificateIssuer() { + return certificateIssuer; + } + + @Override protected CRLIssuer getCrlIssuer() { + return crlIssuer; + } + + public void setOcspResponder(OCSPResponder ocspResponder, String ocspResponderUrl, X509CertificateHolder ocspResponderCertificate) { + this.ocspResponder = ocspResponder; + this.ocspResponderUrl = ocspResponderUrl; + this.ocspResponderCertificate = ocspResponderCertificate; + } + + + @Override public OCSPResponder getOCSPResponder() { + return ocspResponder; + } + + @Override public X509CertificateHolder getOCSPResponderCertificate() { + return ocspResponderCertificate; + } + + @Override public String getCaAlgorithm() { + return certificateIssuer.getCertificateIssuerModel().getAlgorithm(); + } + + @Override public List getCrlDpURLs() { + return crlDistributionPoints; + } + + @Override public String getOCSPResponderURL() { + return ocspResponderUrl; + } + + @Override protected DefaultCertificateModelBuilder getBaseCertificateModelBuilder(CertNameModel subject, PublicKey publicKey, + X509CertificateHolder issuerCertificate, CertificateIssuerModel certificateIssuerModel) throws CertificateIssuanceException { + DefaultCertificateModelBuilder certModelBuilder = DefaultCertificateModelBuilder.getInstance(publicKey, getCaCertificate(), + certificateIssuerModel); + certModelBuilder + .subject(subject) + .includeAki(true) + .includeSki(true) + .basicConstraints(new BasicConstraintsModel(true, true)) + .keyUsage(new KeyUsageModel(KeyUsage.digitalSignature)) + .crlDistributionPoints(crlDistributionPoints.isEmpty() ? null : crlDistributionPoints) + .ocspServiceUrl(ocspResponder != null ? ocspResponderUrl : null) + .authenticationContext(SAMLAuthContextBuilder.instance() + .assertionRef("1234567890") + .serviceID("SignService") + .authenticationInstant(new Date()) + .authnContextClassRef("http://id.example.com/loa3") + .attributeMappings(Arrays.asList(AttributeMappingBuilder.instance() + .friendlyName("commonName") + .name("urn:oid:2.5.4.3") + .nameFormat("http://example.com/nameFormatUri") + .ref("1.2.3.4") + .type(AttributeRefType.rdn) + .build())) + .build()); + return certModelBuilder; + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/TestServices.java b/src/test/java/se/swedenconnect/ca/cmc/ca/TestServices.java new file mode 100644 index 0000000..078831a --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/TestServices.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import se.swedenconnect.ca.cmc.utils.TestUtils; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * This is the top level class for accessing test data and services for unit testing + * + * The structure for CA services is the following + * - TestCAHolder is a super class for a CA service and related data + * - TestCAService holds a CA service. The CA Service in turn consist of a Certificate issuer component that creates the certificates + * and the CA repository and revocation services. + * - The TestCARepository is a simple implementation of a repository used to store information about issued Certificates + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Slf4j +public class TestServices { + @Getter private static Map testCAs; + @Getter private static Map certValidators; + private static List trustAnchors; + public static KeyPair rsa2048kp01; + public static KeyPair rsa2048kp02; + public static KeyPair rsa3072kp; + public static KeyPair ec256kp01; + public static KeyPair ec256kp02; + public static KeyPair ec521kp; + + static { + testCAs = new HashMap<>(); + certValidators = new HashMap<>(); + trustAnchors = new ArrayList<>(); + + try { + + // Generate user key pais + log.info("Generating rsa 2048 user key"); + rsa2048kp01 = TestUtils.generateRSAKeyPair(2048); + log.info("Generating rsa 2048 user key"); + rsa2048kp02 = TestUtils.generateRSAKeyPair(2048); + log.info("Generating rsa 3072 user key"); + rsa3072kp = TestUtils.generateRSAKeyPair(3072); + log.info("Generating ec P256 user key"); + ec256kp01 = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + log.info("Generating ec P256 user key"); + ec256kp02 = TestUtils.generateECKeyPair(TestUtils.NistCurve.P256); + log.info("Generating ec P521 user key"); + ec521kp = TestUtils.generateECKeyPair(TestUtils.NistCurve.P521); + + } + catch (Exception ignored) { + } + } + + @SneakyThrows + public static void addCa(TestCA caConfig) { + TestCAHolder testCAHolder = new TestCAHolder(caConfig); + testCAs.put(caConfig, testCAHolder); + trustAnchors.add(TestUtils.getCertificate(testCAHolder.getCscaService().getCaCertificate().getEncoded())); + } + + public static void addValidators(boolean singleThreaded) { + certValidators = new HashMap<>(); + Arrays.stream(ValidatorProfile.values()).forEach(profile -> certValidators.put(profile, getValidator(profile, singleThreaded))); + } + + @SneakyThrows + public static CertValidatorComponents getValidator(ValidatorProfile profile, boolean singleThreaded) { + return TestValidatorFactory.getCertificateValidator(trustAnchors, profile, singleThreaded ); + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/TestValidatorFactory.java b/src/test/java/se/swedenconnect/ca/cmc/ca/TestValidatorFactory.java new file mode 100644 index 0000000..2a16a2a --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/TestValidatorFactory.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ocsp.OCSPRequest; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.util.encoders.Base64; +import se.idsec.sigval.cert.chain.AbstractPathValidator; +import se.idsec.sigval.cert.chain.impl.CertificatePathValidator; +import se.idsec.sigval.cert.chain.impl.CertificatePathValidatorFactory; +import se.idsec.sigval.cert.chain.impl.CertificateValidityCheckerFactory; +import se.idsec.sigval.cert.chain.impl.StatusCheckingCertificateValidatorImpl; +import se.idsec.sigval.cert.validity.CertificateValidityChecker; +import se.idsec.sigval.cert.validity.crl.CRLCache; +import se.idsec.sigval.cert.validity.crl.impl.CRLCacheImpl; +import se.idsec.sigval.cert.validity.crl.impl.CRLDataLoader; +import se.idsec.sigval.cert.validity.impl.BasicCertificateValidityChecker; +import se.idsec.sigval.cert.validity.ocsp.OCSPCertificateVerifier; +import se.idsec.sigval.cert.validity.ocsp.OCSPDataLoader; +import se.swedenconnect.ca.cmc.utils.TestUtils; +import se.swedenconnect.ca.engine.revocation.ocsp.OCSPResponder; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertStore; +import java.security.cert.CertificateException; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Providing certificate validator for certificates issued by a CA provider + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class TestValidatorFactory { + + /** + * Creates and returns the components of a certificate validator. These components are the actual certificate validator and the CRL cache + * used by the certificate validator. The profiles regulate whether a certificate validator according the the profile should be responsive + * or not. + * + * @param trustedCerts The list of trusted certificates trusted by this validator + * @param profile profile determining the function of the validator + * @param singleThreaded true to run the certificate validator in singlethreaded mode + * @return Certificate validator components + * @throws IOException On error + * @throws CertificateException On error + */ + public static CertValidatorComponents getCertificateValidator(List trustedCerts, ValidatorProfile profile, boolean singleThreaded) + throws IOException, CertificateException { + + File crlCacheDir = new File(System.getProperty("user.dir"), "target/test/crl-cache"); + CRLCache crlCache = new CRLCacheImpl(crlCacheDir, 0, new TestCRLDataLoader(profile)); + + StatusCheckingCertificateValidatorImpl certificateValidator = new StatusCheckingCertificateValidatorImpl( + crlCache, null, trustedCerts.toArray(new X509Certificate[0])); + certificateValidator.setCertificatePathValidatorFactory( + new TestCertificatePathValidatorFactory(singleThreaded, profile)); + certificateValidator.setSingleThreaded(singleThreaded); + return new CertValidatorComponents(certificateValidator, crlCache); + } + + public static class TestCertificatePathValidatorFactory implements CertificatePathValidatorFactory { + private final boolean singleThreaded; + private final ValidatorProfile profile; + + public TestCertificatePathValidatorFactory(boolean singleThreaded, ValidatorProfile profile) { + this.singleThreaded = singleThreaded; + this.profile = profile; + } + + @Override public AbstractPathValidator getPathValidator(X509Certificate targetCert, List chain, + List trustAnchors, CertStore certStore, CRLCache crlCache) { + CertificatePathValidator pathValidator = new CertificatePathValidator(targetCert, chain, trustAnchors, certStore, crlCache); + if (singleThreaded) { + pathValidator.setSingleThreaded(true); + } + else { + pathValidator.setMaxValidationSeconds(150); + } + pathValidator.setCertificateValidityCheckerFactory(new TestCertificateValidityCheckerFactory(profile)); + return pathValidator; + } + } + + public static class TestCertificateValidityCheckerFactory implements CertificateValidityCheckerFactory { + private final ValidatorProfile profile; + + public TestCertificateValidityCheckerFactory(ValidatorProfile profile) { + this.profile = profile; + } + + @Override public CertificateValidityChecker getCertificateValidityChecker(X509Certificate certificate, X509Certificate issuer, + CRLCache crlCache, PropertyChangeListener... propertyChangeListeners) { + BasicCertificateValidityChecker validityChecker = new BasicCertificateValidityChecker(certificate, issuer, crlCache, + propertyChangeListeners); + validityChecker.setSingleThreaded(true); + validityChecker.getValidityCheckers().stream() + .filter(vc -> vc instanceof OCSPCertificateVerifier) + .map(vc -> (OCSPCertificateVerifier) vc) + .forEach(ocspCertificateVerifier -> ocspCertificateVerifier.setOcspDataLoader(new TestOCSPDataLoader(profile))); + return validityChecker; + } + } + + public static class TestOCSPDataLoader implements OCSPDataLoader { + @Getter private String lastResponseB64; + @Setter private boolean enforceUrlMatch = true; + private final ValidatorProfile profile; + + public TestOCSPDataLoader(ValidatorProfile profile) { + this.profile = profile; + } + + @Override public OCSPResp requestOCSPResponse(String url, OCSPReq ocspReq, int connectTimeout, int readTimeout) throws IOException { + TestCAHolder caHolder = getTestCSCAService(ocspReq); + TestCAService cscaService = caHolder.getCscaService(); + OCSPResponder ocspResponder = cscaService.getOCSPResponder(); + if (cscaService.getOCSPResponderURL().equals(url) || !enforceUrlMatch) { + OCSPResp ocspResp = ocspResponder.handleRequest( + OCSPRequest.getInstance(new ASN1InputStream(ocspReq.getEncoded()).readObject())); + lastResponseB64 = Base64.toBase64String(ocspResp.getEncoded()); + switch (profile) { + case NONE_RESPONSIVE: + return null; + default: + return ocspResp; + } + } + throw new IOException("Unable to get OCSP response on requested URL"); + } + + @SneakyThrows + private TestCAHolder getTestCSCAService(OCSPReq ocspReq) { + CertificateID certID = ocspReq.getRequestList()[0].getCertID(); + + Map testCAHolderMap = TestServices.getTestCAs(); + Set testCAS = testCAHolderMap.keySet(); + for (TestCA testCa : testCAS) { + TestCAHolder testCAHolder = testCAHolderMap.get(testCa); + X509Certificate issuer = TestUtils.getCertificate(testCAHolder.getCscaService().getCaCertificate().getEncoded()); + DigestCalculator digestCalculator = new JcaDigestCalculatorProviderBuilder().build().get(CertificateID.HASH_SHA1); + CertificateID matchCertificateId = new CertificateID(digestCalculator, new JcaX509CertificateHolder(issuer), BigInteger.ONE); + if (matchCertId(certID, matchCertificateId)) { + return testCAHolder; + } + } + return null; + } + + private boolean matchCertId(CertificateID certID, CertificateID matchCertificateId) { + boolean nameMatch = Arrays.equals(certID.getIssuerNameHash(), matchCertificateId.getIssuerNameHash()); + boolean keyMatch = Arrays.equals(certID.getIssuerKeyHash(), matchCertificateId.getIssuerKeyHash()); + return nameMatch && keyMatch; + } + } + + public static class TestCRLDataLoader implements CRLDataLoader { + private final ValidatorProfile profile; + + public TestCRLDataLoader(ValidatorProfile profile) { + this.profile = profile; + } + + @Override public byte[] downloadCrl(String url, int connectTimeout, int readTimeout) throws IOException { + if (url.startsWith(TestCAHolder.FILE_URL_PREFIX)) { + String urlEncodedPath = url.substring(TestCAHolder.FILE_URL_PREFIX.length()); + String filePath = URLDecoder.decode(urlEncodedPath, StandardCharsets.UTF_8); + File crlFile = new File(filePath); + switch (profile) { + case NONE_RESPONSIVE: + return null; + default: + return FileUtils.readFileToByteArray(crlFile); + } + } + throw new IOException("Illegal file path URL"); + } + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/ca/ValidatorProfile.java b/src/test/java/se/swedenconnect/ca/cmc/ca/ValidatorProfile.java new file mode 100644 index 0000000..471008f --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/ca/ValidatorProfile.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.ca; + +/** + * Enumeration of functional profiles for certificate validators + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public enum ValidatorProfile { + + /** Normal operation where all validators are responsive to requests */ + NORMAL, + /** Certificate validators do not respond to CRL download or OCSP requests */ + NONE_RESPONSIVE; +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/data/CMCRequestData.java b/src/test/java/se/swedenconnect/ca/cmc/data/CMCRequestData.java new file mode 100644 index 0000000..f969f70 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/data/CMCRequestData.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.data; + +import com.fasterxml.jackson.core.JsonProcessingException; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.AdminRequestType; +import se.swedenconnect.ca.cmc.model.admin.request.ListCerts; +import se.swedenconnect.ca.cmc.utils.TestUtils; +import se.swedenconnect.ca.engine.ca.attribute.CertAttributes; +import se.swedenconnect.ca.engine.ca.models.cert.AttributeTypeAndValueModel; +import se.swedenconnect.ca.engine.ca.models.cert.CertNameModel; +import se.swedenconnect.ca.engine.ca.models.cert.impl.ExplicitCertNameModel; +import se.swedenconnect.ca.engine.ca.repository.SortBy; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCRequestData { + + public static final String USER1 = "User1"; + public static final String USER2 = "User2"; + public static final String USER3 = "User3"; + public static final String PKCS10_USER = "User4"; + public static final String CRMF_USER = "User5"; + public static final String LIST_CERTS = "listCerts"; + public static final String CA_INFO = "caInfo"; + public static final String LIST_CERT_SERIALS = "listSerials"; + + public static Map subjectMap; + public static Map adminRequestMap; + + + static { + subjectMap = new HashMap<>(); + subjectMap.put(USER1, new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.C).value("SE").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.CN).value("Test User One").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SERIALNUMBER).value("12345678901").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.GIVENNAME).value("Test User").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SURNAME).value("One").build() + ))); + subjectMap.put(USER2, new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.C).value("SE").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.CN).value("Test User Two").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SERIALNUMBER).value("12345678902").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.GIVENNAME).value("Test User").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SURNAME).value("Two").build() + ))); + subjectMap.put(USER3, new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.C).value("SE").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.CN).value("Test User Three").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SERIALNUMBER).value("12345678903").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.GIVENNAME).value("Test User").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SURNAME).value("Three").build() + ))); + subjectMap.put(PKCS10_USER, new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.C).value("SE").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.CN).value("PKCS10 User Four").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SERIALNUMBER).value("12345678903").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.GIVENNAME).value("PKCS10 User").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SURNAME).value("Four").build() + ))); + subjectMap.put(CRMF_USER, new ExplicitCertNameModel(Arrays.asList( + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.C).value("SE").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.CN).value("CRMF User Five").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SERIALNUMBER).value("12345678903").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.GIVENNAME).value("CRMF User").build(), + AttributeTypeAndValueModel.builder().attributeType(CertAttributes.SURNAME).value("Five").build() + ))); + + adminRequestMap = new HashMap<>(); + try { + adminRequestMap.put(LIST_CERTS, AdminCMCData.builder() + .adminRequestType(AdminRequestType.listCerts) + .data(TestUtils.OBJECT_MAPPER.writeValueAsString(ListCerts.builder() + .pageIndex(0) + .pageSize(10) + .notRevoked(false) + .sortBy(SortBy.issueDate) + .build())) + .build()); + adminRequestMap.put(CA_INFO, AdminCMCData.builder().adminRequestType(AdminRequestType.caInfo).build()); + adminRequestMap.put(LIST_CERT_SERIALS, AdminCMCData.builder().adminRequestType(AdminRequestType.allCertSerials).build()); + + } + catch (JsonProcessingException e) { + e.printStackTrace(); + } + + } + + + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/data/TestResponseStatus.java b/src/test/java/se/swedenconnect/ca/cmc/data/TestResponseStatus.java new file mode 100644 index 0000000..3291719 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/data/TestResponseStatus.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bouncycastle.asn1.cmc.BodyPartID; +import se.swedenconnect.ca.cmc.api.data.CMCFailType; +import se.swedenconnect.ca.cmc.api.data.CMCResponseStatus; +import se.swedenconnect.ca.cmc.api.data.CMCStatusType; + +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@AllArgsConstructor +public enum TestResponseStatus { + + success(CMCResponseStatus.builder() + .status(CMCStatusType.success) + .build()), + failBadRequest(CMCResponseStatus.builder() + .status(CMCStatusType.failed) + .failType(CMCFailType.badRequest) + .message("Bad CMC Request") + .build()); + + private CMCResponseStatus responseStatus; + + public CMCResponseStatus withBodyParts(List bodyPartIDList){ + CMCResponseStatus status = CMCResponseStatus.builder() + .status(responseStatus.getStatus()) + .failType(responseStatus.getFailType()) + .message(responseStatus.getMessage()) + .bodyPartIDList(bodyPartIDList) + .build(); + return status; + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/utils/CMCDataPrint.java b/src/test/java/se/swedenconnect/ca/cmc/utils/CMCDataPrint.java new file mode 100644 index 0000000..0873330 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/utils/CMCDataPrint.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.utils; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.asn1.crmf.CertReqMsg; +import org.bouncycastle.cert.crmf.CertificateRequestMessage; +import org.bouncycastle.util.encoders.Base64; +import se.swedenconnect.ca.cmc.api.data.*; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCDataPrint { + + public static String printCMCRequest(CMCRequest cmcRequest, boolean includeFullMessage, boolean includeCertRequest) { + + if (cmcRequest == null) { + return "Null CMC Request"; + } + + try { + StringBuilder b = new StringBuilder(); + String cmcBase64 = Base64.toBase64String(cmcRequest.getCmcRequestBytes()); + CMCRequestType cmcRequestType = cmcRequest.getCmcRequestType(); + b.append("CMC request type: ").append(cmcRequestType).append("\n"); + PKIData pkiData = cmcRequest.getPkiData(); + b.append(" time: ").append(CMCUtils.getSigningTime(cmcRequest.getCmcRequestBytes())).append("\n"); + TaggedAttribute[] controlSequence = pkiData.getControlSequence(); + if (controlSequence.length > 0) { + b.append("CMC Control sequence (size=").append(controlSequence.length).append(")\n"); + for (TaggedAttribute csAttr : controlSequence) { + CMCControlObjectID controlObjectID = CMCControlObjectID.getControlObjectID(csAttr.getAttrType()); + b.append(" type: ").append(controlObjectID).append("\n"); + printControlValue(cmcRequestType, controlObjectID, csAttr, b); + } + } + + switch (cmcRequestType) { + + case issueCert: + printIssueCert(pkiData, includeCertRequest, b); + break; + } + + if (includeFullMessage) { + b.append(" Full CMC request:\n").append(base64Print(cmcRequest.getCmcRequestBytes(), 120)).append("\n"); + } + + return b.toString(); + } catch (Exception ex) { + return "Error parsing CMC request: " + ex.toString() + "\n"; + } + } + + public static String printCMCResponse(CMCResponse cmcResponse, boolean includeFullMessage) { + if (cmcResponse == null) { + return "Null CMC Request"; + } + + try { + StringBuilder b = new StringBuilder(); + String cmcBase64 = Base64.toBase64String(cmcResponse.getCmcResponseBytes()); + PKIResponse pkiResponse = cmcResponse.getPkiResponse(); + TaggedAttribute[] responseControlSequence = CMCUtils.getResponseControlSequence(pkiResponse); + b.append("CMC Request type: ").append(cmcResponse.getCmcRequestType()).append("\n"); + b.append(" time: ").append(CMCUtils.getSigningTime(cmcResponse.getCmcResponseBytes())).append("\n"); + if (responseControlSequence.length > 0) { + b.append("CMC Control sequence (size=").append(responseControlSequence.length).append(")\n"); + for (TaggedAttribute csAttr: responseControlSequence){ + CMCControlObjectID controlObjectID = CMCControlObjectID.getControlObjectID(csAttr.getAttrType()); + b.append(" type: ").append(controlObjectID).append("\n"); + printControlValue(null, controlObjectID, csAttr, b); + } + } + + List returnCertificates = cmcResponse.getReturnCertificates(); + if (returnCertificates != null) { + for (X509Certificate certificate: returnCertificates){ + b.append(" ReturnCert: ").append(certificate.getSubjectX500Principal()).append("\n"); + b.append(" Certificate bytes:\n").append(base64Print(certificate.getEncoded(), 120)).append("\n"); + } + } + + if (includeFullMessage) { + b.append(" Full CMC response:\n").append(base64Print(cmcResponse.getCmcResponseBytes(), 120)).append("\n"); + } + + return b.toString(); + } catch (Exception ex) { + return "Error parsing CMC request: " + ex.toString() + "\n"; + } + } + + + private static void printControlValue(CMCRequestType cmcRequestType, CMCControlObjectID controlObjectID, TaggedAttribute csAttr, + StringBuilder b) { + ASN1Set attrValues = csAttr.getAttrValues(); + for (int i = 0; i < attrValues.size(); i++) { + ASN1Encodable asn1Encodable = attrValues.getObjectAt(i); + String valueStr = ""; + try { + switch (controlObjectID) { + + case senderNonce: + case recipientNonce: + valueStr = Base64.toBase64String(ASN1OctetString.getInstance(asn1Encodable).getOctets()); + b.append(" value: ").append(valueStr).append("\n"); + break; + case regInfo: + byte[] octets = ASN1OctetString.getInstance(asn1Encodable).getOctets(); + if (cmcRequestType == null) { + break; + } + switch (cmcRequestType) { + case issueCert: + valueStr = new String(octets, StandardCharsets.UTF_8); + b.append(" value: ").append(valueStr).append("\n"); + break; + case revoke: + case getCert: + valueStr = Base64.toBase64String(octets); + b.append(" value: ").append(valueStr).append("\n"); + break; + case admin: + AdminCMCData adminRequestData = TestUtils.OBJECT_MAPPER.readValue(octets, AdminCMCData.class); + b.append(" admin-type: ").append(adminRequestData.getAdminRequestType()).append("\n"); + String requestData = adminRequestData.getData(); + if (requestData != null) { + valueStr = TestUtils.OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( + TestUtils.OBJECT_MAPPER.readValue(requestData, Object.class) + ); + b.append(" request-data:\n").append(valueStr.replaceAll("(?m)^", " ")).append("\n"); + } + break; + } + break; + case lraPOPWitness: + LraPopWitness lraPopWitness = LraPopWitness.getInstance(asn1Encodable); + BodyPartID[] bodyIds = lraPopWitness.getBodyIds(); + for (BodyPartID bodyPartID : bodyIds) { + b.append(" POP witness ID: ").append(bodyPartID.getID()).append("\n"); + } + break; + case getCert: + GetCert getCert = GetCert.getInstance(asn1Encodable); + String issuerName = getCert.getIssuerName().toString(); + String certSerial = getCert.getSerialNumber().toString(16); + b.append(" cert-serial: ").append(certSerial).append("\n"); + b.append(" issuer: ").append(issuerName).append("\n"); + break; + case revokeRequest: + RevokeRequest revokeRequest = RevokeRequest.getInstance(asn1Encodable); + b.append(" cert-serial: ").append(revokeRequest.getSerialNumber().toString(16)).append("\n"); + b.append(" ").append(revokeRequest.getReason()).append("\n"); + b.append(" date: ").append(revokeRequest.getInvalidityDate().getDate()).append("\n"); + b.append(" issuer: ").append(revokeRequest.getName()).append("\n"); + break; + case statusInfoV2: + CMCStatusInfoV2 statusInfoV2 = CMCStatusInfoV2.getInstance(asn1Encodable); + CMCFailType cmcFailType = getCmcFailType(statusInfoV2); + CMCStatusType cmcStatus = CMCStatusType.getCMCStatusType(statusInfoV2.getcMCStatus()); + DERUTF8String statusString = statusInfoV2.getStatusString(); + b.append(" CMC status: ").append(cmcStatus).append("\n"); + BodyPartID[] bodyList = statusInfoV2.getBodyList(); + for (BodyPartID bodyPartID:bodyList) { + b.append(" Processed object: ").append(bodyPartID.getID()).append("\n"); + } + if (cmcFailType != null){ + b.append(" CMC fail info: ").append(cmcFailType).append("\n"); + } + if (statusString != null) { + b.append(" status string: ").append(statusString.getString()).append("\n"); + } + break; + case responseInfo: + byte[] responseInfoData = ASN1OctetString.getInstance(asn1Encodable).getOctets(); + try { + AdminCMCData adminCMCData = CMCUtils.OBJECT_MAPPER.readValue(responseInfoData, AdminCMCData.class); + b.append(" admin-type: ").append(adminCMCData.getAdminRequestType()).append("\n"); + String responseData = adminCMCData.getData(); + if (responseData != null) { + valueStr = TestUtils.OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString( + TestUtils.OBJECT_MAPPER.readValue(responseData, Object.class) + ); + b.append(" response-data:\n").append(valueStr.replaceAll("(?m)^", " ")).append("\n"); + } + } catch (Exception ex) { + // This was not admin json data. Check if this is a string value + String responseInfoString = TestUtils.getStringRepresentation(responseInfoData); + b.append(" response-data: ").append(responseInfoString).append("\n"); + } + break; + default: + b.append(" Encoded control data: ") + .append(Base64.toBase64String(asn1Encodable.toASN1Primitive().getEncoded(ASN1Encoding.DER))) + .append("\n"); + break; + } + } + catch (Exception ex) { + b.append(" value-error: ").append(ex.toString()).append("\n"); + b.append(" value: ").append(valueStr).append("\n"); + } + } + } + + public static CMCFailType getCmcFailType(CMCStatusInfoV2 statusInfoV2) { + OtherStatusInfo otherStatusInfo = statusInfoV2.getOtherStatusInfo(); + if (otherStatusInfo != null && otherStatusInfo.isFailInfo()){ + CMCFailInfo cmcFailInfo = CMCFailInfo.getInstance(otherStatusInfo.toASN1Primitive()); + return CMCFailType.getCMCFailType(cmcFailInfo); + } + return null; + } + + private static void printIssueCert(PKIData pkiData, boolean includeCertRequest, StringBuilder b) throws IOException { + TaggedRequest[] reqSequence = pkiData.getReqSequence(); + for (TaggedRequest taggedRequest : reqSequence) { + ASN1Encodable taggedRequestValue = taggedRequest.getValue(); + if (taggedRequestValue instanceof TaggedCertificationRequest) { + TaggedCertificationRequest taggedCertReq = (TaggedCertificationRequest) taggedRequestValue; + ASN1Sequence taggedCertReqSeq = ASN1Sequence.getInstance(taggedCertReq.toASN1Primitive()); + BodyPartID certReqBodyPartId = BodyPartID.getInstance(taggedCertReqSeq.getObjectAt(0)); + CertificationRequest certificationRequest = CertificationRequest.getInstance(taggedCertReqSeq.getObjectAt(1)); + b.append(" Certificate request: PKCS#10 Certificate Request\n"); + b.append(" Body part ID: ").append(certReqBodyPartId.getID()).append("\n"); + if (includeCertRequest) { + b.append(" Certificate Request:\n").append(base64Print(certificationRequest.getEncoded(ASN1Encoding.DER), 120)).append("\n"); + } + return; + } + if (taggedRequestValue instanceof CertReqMsg) { + CertificateRequestMessage certificateRequestMessage = new CertificateRequestMessage((CertReqMsg) taggedRequestValue); + ASN1Integer certReqId = ((CertReqMsg) taggedRequestValue).getCertReq().getCertReqId(); + BodyPartID certReqBodyPartId = new BodyPartID(certReqId.longValueExact()); + b.append(" Certificate request: CRMF Certificate Request Message\n"); + b.append(" Body part ID: ").append(certReqBodyPartId.getID()).append("\n"); + if (includeCertRequest){ + b.append(" Certificate Request:\n").append(base64Print(certificateRequestMessage.getEncoded(), 120)).append("\n"); + } + return; + } + b.append(" Certificate request: Unknown request type\n"); + } + } + + private static String base64Print(byte[] data, int width) { + // Create a String with linebreaks + String b64String = Base64.toBase64String(data).replaceAll("(.{" + width + "})", "$1\n"); + // Ident string with 6 spaces + return b64String.replaceAll("(?m)^", " "); + } + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/utils/CMCDataValidator.java b/src/test/java/se/swedenconnect/ca/cmc/utils/CMCDataValidator.java new file mode 100644 index 0000000..39cd7f3 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/utils/CMCDataValidator.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.utils; + +import com.fasterxml.jackson.databind.type.CollectionType; +import org.bouncycastle.asn1.cmc.*; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.crmf.CertificateRequestMessage; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedData; +import se.swedenconnect.ca.cmc.api.data.CMCControlObject; +import se.swedenconnect.ca.cmc.api.data.CMCRequest; +import se.swedenconnect.ca.cmc.api.data.CMCResponse; +import se.swedenconnect.ca.cmc.api.data.CMCResponseStatus; +import se.swedenconnect.ca.cmc.auth.CMCUtils; +import se.swedenconnect.ca.cmc.model.admin.AdminCMCData; +import se.swedenconnect.ca.cmc.model.admin.AdminRequestType; +import se.swedenconnect.ca.cmc.model.admin.request.ListCerts; +import se.swedenconnect.ca.cmc.model.admin.response.CAInformation; +import se.swedenconnect.ca.cmc.model.admin.response.CertificateData; +import se.swedenconnect.ca.cmc.model.request.CMCRequestModel; +import se.swedenconnect.ca.cmc.model.request.CMCRequestType; +import se.swedenconnect.ca.cmc.model.request.impl.CMCAdminRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCCertificateRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCGetCertRequestModel; +import se.swedenconnect.ca.cmc.model.request.impl.CMCRevokeRequestModel; +import se.swedenconnect.ca.cmc.model.response.CMCResponseModel; +import se.swedenconnect.ca.engine.utils.CAUtils; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCDataValidator { + + public static CollectionType certDataListType = CMCUtils.OBJECT_MAPPER.getTypeFactory() + .constructCollectionType(List.class, CertificateData.class); + public static CollectionType bigIntListType = CMCUtils.OBJECT_MAPPER.getTypeFactory() + .constructCollectionType(List.class, BigInteger.class); + + public static void validateCMCRequest(CMCRequest cmcRequest, CMCRequestModel requestModel) throws IOException, ParseException { + CMCRequestType cmcRequestType = requestModel.getCmcRequestType(); + PKIData pkiData = cmcRequest.getPkiData(); + + // Check nonce + if (!Arrays.equals(cmcRequest.getNonce(), requestModel.getNonce())) { + throw new IOException("Nonce mismatch"); + } + + // Check request type + if (cmcRequestType == null || !cmcRequestType.equals(cmcRequest.getCmcRequestType())) { + throw new IOException("Request type mismatch"); + } + + // Check cert request data + if (CMCRequestType.issueCert.equals(cmcRequestType)) { + CertificationRequest p10Request = cmcRequest.getCertificationRequest(); + CertificateRequestMessage crmfRequest = cmcRequest.getCertificateRequestMessage(); + if (p10Request == null && crmfRequest == null) { + throw new IOException("No valid request"); + } + CMCCertificateRequestModel certReqModel = (CMCCertificateRequestModel) requestModel; + if (certReqModel.getCertReqPrivate() == null && certReqModel.getP10Algorithm() == null) { + if (crmfRequest == null) { + throw new IOException("No cert request signing key and algorithm. Request must be CRMF but no such request was created"); + } + } + else { + if (p10Request == null) { + throw new IOException( + "Cert request private key and/or cert request signing algorithm was provided, but no PKCS#10 request was created"); + } + } + + // Ensure that a cert request body part ID was recorded + long certReqBPIDVal = cmcRequest.getCertReqBodyPartId().getID(); + + // Check LRA pop whiteness + if (certReqModel.isLraPopWitness()) { + CMCControlObject lpwObj = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_lraPOPWitness, pkiData); + LraPopWitness lpw = (LraPopWitness) lpwObj.getValue(); + BodyPartID[] bodyIds = lpw.getBodyIds(); + if (certReqBPIDVal != bodyIds[0].getID()) { + throw new IOException("LRA POP Witness cert request ID does not match cert request"); + } + } + } + + // Check revocation request data + if (CMCRequestType.revoke.equals(cmcRequestType)) { + RevokeRequest revokeRequest = (RevokeRequest) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_revokeRequest, pkiData) + .getValue(); + CMCRevokeRequestModel revokeReqModel = (CMCRevokeRequestModel) requestModel; + if (!revokeReqModel.getIssuerName().equals(revokeRequest.getName())) { + throw new IOException("Certificate issuer DN mismatch in CMC revoke request"); + } + if (!revokeReqModel.getSerialNumber().equals(revokeRequest.getSerialNumber())) { + throw new IOException("Certificate serial number mismatch in CMC revoke request"); + } + long modelRevTimeSec = revokeReqModel.getRevocationDate().getTime() / 1000; + long revTimeSec = revokeRequest.getInvalidityDate().getDate().getTime() / 1000; + if (modelRevTimeSec != revTimeSec) { + Date modelRevocationDate = revokeReqModel.getRevocationDate(); + Date revokeDate = revokeRequest.getInvalidityDate().getDate(); + throw new IOException( + "Certificate revocation date mismatch CMC revoke request - expected: " + modelRevocationDate + " found: " + revokeDate); + } + if (revokeReqModel.getReason() != revokeRequest.getReason().getValue().intValue()) { + throw new IOException("Certificate serial number mismatch in CMC revoke request"); + } + } + + // Check get cert data + if (CMCRequestType.getCert.equals(cmcRequestType)) { + GetCert getCert = (GetCert) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_getCert, pkiData).getValue(); + CMCGetCertRequestModel getCertReqModel = (CMCGetCertRequestModel) requestModel; + + if (!getCertReqModel.getIssuerName().equals(X500Name.getInstance(getCert.getIssuerName().getName()))) { + throw new IOException("Certificate issuer DN mismatch in CMC get cert request"); + } + if (!getCertReqModel.getSerialNumber().equals(getCert.getSerialNumber())) { + throw new IOException("Certificate serial number mismatch in CMC get cert request"); + } + } + + // Check admin request data + if (CMCRequestType.admin.equals(cmcRequestType)) { + AdminCMCData adminCMCData = (AdminCMCData) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_regInfo, pkiData).getValue(); + CMCAdminRequestModel adminReqModel = (CMCAdminRequestModel) requestModel; + AdminRequestType adminRequestType = adminCMCData.getAdminRequestType(); + if (adminRequestType == null) { + throw new IOException("Admin request type must be specified in Admin request data"); + } + AdminCMCData modelAdminData = CMCUtils.OBJECT_MAPPER.readValue(adminReqModel.getRegistrationInfo(), AdminCMCData.class); + + if (!adminRequestType.equals(modelAdminData.getAdminRequestType())) { + throw new IOException("Admin request type mismatch"); + } + switch (adminRequestType) { + case caInfo: + if (adminCMCData.getData() != null) { + throw new IOException("Illegal admin request data for ca info request - Expected null"); + } + break; + case allCertSerials: + if (adminCMCData.getData() != null) { + throw new IOException("Illegal admin request data for all cert serial request - Expected null"); + } + break; + case listCerts: + ListCerts listCerts = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), ListCerts.class); + ListCerts modelListCerts = CMCUtils.OBJECT_MAPPER.readValue(modelAdminData.getData(), ListCerts.class); + if (listCerts.isNotRevoked() ^ modelListCerts.isNotRevoked()) { + throw new IOException("Admin request data for list cert mismatch - isValid mismatch"); + } + if (listCerts.getPageIndex() != modelListCerts.getPageIndex()) { + throw new IOException("Admin request data for list cert mismatch - pageIndex mismatch"); + } + if (listCerts.getPageSize() != modelListCerts.getPageSize()) { + throw new IOException("Admin request data for list cert mismatch - pageSize mismatch"); + } + if (!listCerts.getSortBy().equals(modelListCerts.getSortBy())) { + throw new IOException("Admin request data for list cert mismatch - sortBy mismatch"); + } + break; + } + } + } + + public static void validateCMCResponse(CMCResponse cmcResponse, CMCResponseModel responseModel) + throws IOException, CMSException, CertificateException { + + PKIResponse pkiResponse = cmcResponse.getPkiResponse(); + + // Check nonce + if (Arrays.compare(cmcResponse.getNonce(), cmcResponse.getNonce()) != 0) { + throw new IOException("Nonce mismatch"); + } + + //Check return certificates + List returnCertificates = responseModel.getReturnCertificates(); + CMSSignedData cmsSignedData = new CMSSignedData(cmcResponse.getCmcResponseBytes()); + Collection certsInCMS = cmsSignedData.getCertificates().getMatches(null); + + if (returnCertificates != null && !returnCertificates.isEmpty()) { + // response model contains return certificates. Verify that all of them is present in the CMS certs + for (X509Certificate returnCert : returnCertificates) { + // Make sure all of these are in the CMS certs field + boolean present = false; + for (X509CertificateHolder certHoldserInCms : certsInCMS) { + X509Certificate certInCms = CAUtils.getCert(certHoldserInCms); + if (certInCms.equals(returnCert)) { + present = true; + } + } + if (!present) { + throw new IOException("Certificate in response model not present in CMS bag of certs"); + } + } + } + + // Check control messages + // Check the mandatory status message + List processedRequestObjects = responseModel.getCmcResponseStatus().getBodyPartIDList(); + CMCStatusInfoV2 statusInfo = (CMCStatusInfoV2) CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_statusInfoV2, + pkiResponse).getValue(); + CMCResponseStatus cmcResponseStatus = responseModel.getCmcResponseStatus(); + if (!cmcResponseStatus.getStatus().getCmcStatus().equals(statusInfo.getcMCStatus())) { + throw new IOException("Response status mismatch"); + } + // Check processed body part ID:s + + for (BodyPartID reqObjId : processedRequestObjects) { + boolean present = Arrays.stream(statusInfo.getBodyList()) + .anyMatch(bodyPartID -> bodyPartID.equals(reqObjId)); + if (!present) { + throw new IOException("Processed body part ID declaration not present"); + } + } + if (cmcResponseStatus.getFailType() != null) { + if (!cmcResponseStatus.getFailType().getCmcFailInfo().equals(CMCDataPrint.getCmcFailType(statusInfo).getCmcFailInfo())) { + throw new IOException("Response fail type mismatch"); + } + } + if (cmcResponseStatus.getMessage() != null) { + if (!cmcResponseStatus.getMessage().equals(statusInfo.getStatusString().getString())) { + throw new IOException("Response fail type mismatch"); + } + } + + // Check response data + CMCControlObject cmcControlObject = CMCUtils.getCMCControlObject(CMCObjectIdentifiers.id_cmc_responseInfo, pkiResponse); + Object respInfoObj = cmcControlObject.getValue(); + if (respInfoObj instanceof AdminCMCData) { + AdminCMCData adminCMCData = (AdminCMCData) respInfoObj; + AdminCMCData modelAdminCMCData = CMCUtils.OBJECT_MAPPER.readValue(responseModel.getResponseInfo(), AdminCMCData.class); + AdminRequestType adminRequestType = adminCMCData.getAdminRequestType(); + if (!adminRequestType.equals(modelAdminCMCData.getAdminRequestType())) { + throw new IOException("Admin data type mismatch"); + } + switch (adminRequestType) { + case caInfo: + CAInformation caInformation = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), CAInformation.class); + CAInformation modelCaInformation = CMCUtils.OBJECT_MAPPER.readValue(modelAdminCMCData.getData(), CAInformation.class); + if (caInformation.getCertificateChain().size() != modelCaInformation.getCertificateChain().size()) { + throw new IOException("CA chain size mismatch"); + } + if (caInformation.getCertificateCount() != modelCaInformation.getCertificateCount()) { + throw new IOException("CA certificate count mismatch"); + } + if (modelCaInformation.getOcspCertificate() != null) { + if (!Arrays.equals(modelCaInformation.getOcspCertificate(), caInformation.getOcspCertificate())) { + throw new IOException("OCSP certificate mismatch"); + } + } + if (modelCaInformation.getValidCertificateCount() != caInformation.getValidCertificateCount()) { + throw new IOException("Valid certificate count mismatch"); + } + break; + case listCerts: + List certDataList = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), certDataListType); + List modelCertDataList = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), certDataListType); + if (certDataList.size() != modelCertDataList.size()) { + throw new IOException("Cert data list size mismatch"); + } + break; + case allCertSerials: + List allSerialsList = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), bigIntListType); + List modelallSerialsList = CMCUtils.OBJECT_MAPPER.readValue(adminCMCData.getData(), bigIntListType); + if (allSerialsList.size() != modelallSerialsList.size()) { + throw new IOException("All cert serials list size mismatch"); + } + break; + } + } + else { + byte[] respInfoBytes = (byte[]) respInfoObj; + if (!Arrays.equals(respInfoBytes, responseModel.getResponseInfo())) { + throw new IOException("Response info bytes mismatch"); + } + } + } +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/utils/CMCSigner.java b/src/test/java/se/swedenconnect/ca/cmc/utils/CMCSigner.java new file mode 100644 index 0000000..edb0abf --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/utils/CMCSigner.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.utils; + +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import se.swedenconnect.ca.engine.configuration.CAAlgorithmRegistry; + +import java.security.*; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.List; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class CMCSigner { + + private final KeyPair signerKeyPair; + private final List signerCertChain; + private final boolean pss; + private ContentSigner contentSigner; + + public CMCSigner(KeyPair signerKeyPair, X509Certificate signerCert, boolean pss) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException { + this.signerKeyPair = signerKeyPair; + this.signerCertChain = Arrays.asList(signerCert); + this.pss = pss; + setContentSigner(); + } + + public CMCSigner(KeyPair signerKeyPair, X509Certificate signerCert) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException { + this.signerKeyPair = signerKeyPair; + this.signerCertChain = Arrays.asList(signerCert); + this.pss = false; + setContentSigner(); + } + + private void setContentSigner() throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException { + PublicKey publicKey = signerKeyPair.getPublic(); + String algo = null; + if (publicKey instanceof RSAPublicKey) { + if (pss) { + algo = CAAlgorithmRegistry.ALGO_ID_SIGNATURE_RSA_SHA256_MGF1; + } else { + algo = CAAlgorithmRegistry.ALGO_ID_SIGNATURE_RSA_SHA256; + } + } else { + algo = CAAlgorithmRegistry.ALGO_ID_SIGNATURE_ECDSA_SHA256; + } + contentSigner = new JcaContentSignerBuilder(CAAlgorithmRegistry.getSigAlgoName(algo)).build(signerKeyPair.getPrivate()); + } + + public ContentSigner getContentSigner() { + return contentSigner; + } + + public List getSignerChain() { + return signerCertChain; + } +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/utils/PEMType.java b/src/test/java/se/swedenconnect/ca/cmc/utils/PEMType.java new file mode 100644 index 0000000..5d8861e --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/utils/PEMType.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Description + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +@Getter +@AllArgsConstructor +public enum PEMType { + certRequest("CERTIFICATE REQUEST"), + newCertRequest("NEW CERTIFICATE REQUEST"), + cert("CERTIFICATE"), + trustedCert("TRUSTED CERTIFICATE"), + x509Cert("X509 CERTIFICATE"), + crl("X509 CRL"), + pkcs7("PKCS7"), + cms("CMS"), + attributeCert("ATTRIBUTE CERTIFICATE"), + ecParams("EC PARAMETERS"), + publicKey("PUBLIC KEY"), + rsaPublicKey("RSA PUBLIC KEY"), + rsaPrivateKey("RSA PRIVATE KEY"), + dsaPrivateKey("DSA PRIVATE KEY"), + ecPrivateKey("EC PRIVATE KEY"), + encryptedPrivateKey("ENCRYPTED PRIVATE KEY"), + privateKey("PRIVATE KEY"); + + private String header; + +} diff --git a/src/test/java/se/swedenconnect/ca/cmc/utils/TestUtils.java b/src/test/java/se/swedenconnect/ca/cmc/utils/TestUtils.java new file mode 100644 index 0000000..9018e32 --- /dev/null +++ b/src/test/java/se/swedenconnect/ca/cmc/utils/TestUtils.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2021. Agency for Digital Government (DIGG) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.swedenconnect.ca.cmc.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.sec.SECObjectIdentifiers; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import se.swedenconnect.ca.cmc.ca.TestCA; +import se.swedenconnect.ca.cmc.ca.TestCAHolder; +import se.swedenconnect.ca.cmc.ca.TestServices; +import se.swedenconnect.ca.engine.ca.models.cert.AttributeTypeAndValueModel; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility functions for test + * + * @author Martin Lindström (martin@idsec.se) + * @author Stefan Santesson (stefan@idsec.se) + */ +public class TestUtils { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static final String ASCII_STR_REGEX = "^([\\w]|[0-9]|[\\s]){1,}$"; + + public static X509Certificate getCertificate(byte[] certBytes) throws CertificateException, IOException { + try (InputStream inStream = new ByteArrayInputStream(certBytes)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(inStream); + } + } + + public static List getChain(byte[] certificate, TestCA testCa) throws CertificateException, IOException { + TestCAHolder caProvider = TestServices.getTestCAs().get(testCa); + List chain = new ArrayList<>(); + chain.add(getCertificate(certificate)); + chain.add(getCertificate(caProvider.getCscaService().getCaCertificate().getEncoded())); + return chain; + } + + public static KeyPair generateRSAKeyPair(int bits) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + return generateKeyPair(KeyType.RSA, bits); + } + + public static KeyPair generateECKeyPair(NistCurve curve) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + return generateKeyPair(KeyType.EC, curve); + } + + public static KeyPair generateKeyPair(KeyType algorithm, Object spec) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + KeyPair kp; + KeyPairGenerator generator; + generator = KeyPairGenerator.getInstance(algorithm.name(), new BouncyCastleProvider()); + if (spec instanceof AlgorithmParameterSpec) { + generator.initialize((AlgorithmParameterSpec) spec); + return generator.generateKeyPair(); + } + if (spec instanceof NistCurve) { + ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(((NistCurve) spec).getCurveName()); + generator.initialize(parameterSpec); + return generator.generateKeyPair(); + } + generator.initialize((int) spec); + return generator.generateKeyPair(); + } + + public static List getAttributeValues(X500Name subject) { + List attrTypeAndValModelList = new ArrayList<>(); + ASN1ObjectIdentifier[] attributeTypes = subject.getAttributeTypes(); + for (ASN1ObjectIdentifier attrType : attributeTypes) { + RDN[] rdNs = subject.getRDNs(attrType); + for (RDN rdn : rdNs) { + AttributeTypeAndValue[] typesAndValues = rdn.getTypesAndValues(); + for (AttributeTypeAndValue typeAndVal : typesAndValues) { + ASN1ObjectIdentifier type = typeAndVal.getType(); + ASN1Encodable value = typeAndVal.getValue(); + attrTypeAndValModelList.add(new AttributeTypeAndValueModel(type, getValue(value))); + } + } + } + return attrTypeAndValModelList; + } + + private static Object getValue(ASN1Encodable value) { + + if (value instanceof DERPrintableString) { + return ((DERPrintableString) value).getString(); + } + if (value instanceof DERUTF8String) { + return ((DERUTF8String) value).getString(); + } + if (value instanceof DERIA5String) { + return ((DERIA5String) value).getString(); + } + if (value instanceof ASN1GeneralizedTime) { + return ((ASN1GeneralizedTime) value).getTimeString().substring(0, 8); + } + return value.toString(); + } + + public static String getCn(X509Certificate certificate) throws CertificateEncodingException { + return getFirstSubjectAttribute(BCStyle.CN, new JcaX509CertificateHolder(certificate)); + } + + private static String getFirstSubjectAttribute(ASN1ObjectIdentifier oid, X509CertificateHolder cert) { + return IETFUtils.valueToString(cert.getSubject().getRDNs(oid)[0].getFirst().getValue()); + } + + public static String getStringRepresentation(byte[] responseInfoData) { + if (responseInfoData == null || responseInfoData.length == 0){ + return ""; + } + + String str = new String(responseInfoData, StandardCharsets.UTF_8); + if (str.matches(ASCII_STR_REGEX)){ + return str; + } + return Base64.toBase64String(responseInfoData); + } + + public enum KeyType { + RSA, EC, ECDSA; + } + + @Getter + @AllArgsConstructor + public enum NistCurve { + P521("P-521", SECObjectIdentifiers.secp521r1), + P384("P-384", SECObjectIdentifiers.secp384r1), + P256("P-256", SECObjectIdentifiers.secp256r1), + P224("P-224", SECObjectIdentifiers.secp224r1), + P192("P-192", SECObjectIdentifiers.secp192r1); + + String curveName; + ASN1ObjectIdentifier curveOid; + } + + public static String getPemFormatedObject(byte[] data, PEMType pemType) throws IOException { + PemObject pemObject = new PemObject(pemType.getHeader(), data); + StringWriter strWr = new StringWriter(); + PemWriter pemWriter = new PemWriter(strWr); + pemWriter.writeObject(pemObject); + pemWriter.close(); + strWr.close(); + PEMParser pp; + return strWr.toString(); + } + + + +} diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml new file mode 100644 index 0000000..75cbcdd --- /dev/null +++ b/src/test/resources/log4j2.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file