diff --git a/README.md b/README.md index 492fc95a..f32355ca 100644 --- a/README.md +++ b/README.md @@ -332,3 +332,24 @@ at `target/site/apidocs/com/siemens/pki/cmpracomponent/main/CmpRaComponent.html` # Acknowledgements This work was partly funded by the German Federal Ministry of Education and Research in the project Quoryptan through grant number **16KIS2033**. + +# Support for Remote Attestation in Certificate Signing Requests (CSRs) + +This branch supports the internet drafts [Use of Remote Attestation with Certification Signing Requests](https://datatracker.ietf.org/doc/draft-ietf-lamps-csr-attestation/) and [Nonce-based Freshness for Remote Attestation in Certificate Signing Requests (CSRs) for the Certification Management Protocol (CMP) and for Enrollment over Secure Transport (EST) draft-ietf-lamps-attestation-freshness](https://datatracker.ietf.org/doc/draft-ietf-lamps-attestation-freshness/). + +## Design + +The [RA configuration interface](src/main/java/com/siemens/pki/cmpracomponent/configuration/Configuration.java) allows to +register an [`com.siemens.pki.cmpracomponent.configuration.RatVerifierAdapter`](src\main\java\com\siemens\pki\cmpracomponent\configuration\RatVerifierAdapter.java) interface to an external Verifier. This interface is called +by a modified [`com.siemens.pki.cmpracomponent.msgprocessing.ServiceImplementation`](src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/ServiceImplementation.java) to obtain a fresh RAT nonce via +GENM/GENREP and also by the [`com.siemens.pki.cmpracomponent.msgprocessing.RaDownstream`](src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/RaDownstream.java) to process the subsequent CRMF template. + +The [Client configuration interface](./src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientContext.java) allows to register an [`com.siemens.pki.cmpclientcomponent.configuration.ClientAttestationContext`](src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientAttestationContext.java) interface to an external Attester. + +The package [`com.siemens.pki.verifieradapter.asn1`](src/main/java/com/siemens/pki/verifieradapter/asn1) supports some ASN.1 definitons from [Use of Remote Attestation with Certification Signing Requests](https://datatracker.ietf.org/doc/draft-ietf-lamps-csr-attestation/). + +## Test case + +The [`com.siemens.pki.cmpclientcomponent.test.TestCrWithRAT`](src/test/java/com/siemens/pki/cmpclientcomponent/test/TestCrWithRAT.java) shows setup and execution of a RAT sequence. + + diff --git a/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientAttestationContext.java b/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientAttestationContext.java new file mode 100644 index 00000000..fd08ac32 --- /dev/null +++ b/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientAttestationContext.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.cmpclientcomponent.configuration; + +import com.siemens.pki.verifieradapter.asn1.EvidenceBundle; +import com.siemens.pki.verifieradapter.asn1.EvidenceStatement; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue.NonceResponse; +import java.math.BigInteger; +import org.bouncycastle.asn1.x509.Certificate; + +/** + * attestation specific configuration + * + */ +public interface ClientAttestationContext { + + /** + * obtain evidence statement from attestation + * @param attestationNonce DER encoded {@link NonceResponse} + * @return DER encoded {@link EvidenceStatement} + */ + byte[] getEvidenceStatement(byte[] attestationNonce); + + /** + * indicates which Verifier to request a nonce from + * @return hint or null + */ + default String getNonceRequestHint() { + return null; + } + + /** + * indicates the required length of the requested nonce + * @return length or null + */ + default BigInteger getNonceRequestLen() { + return null; + } + /** + * indicates which Evidence type to request a nonce for + * @return OID formatted string or null + */ + default String getNonceRequestType() { + return null; + } + + /** + * Siemens proprietary extension to carry additional data + * @return additional data or null + */ + default byte[] getNonceRequestVendorextension() { + return null; + } + /** + * get certs to include in {@link EvidenceBundle} + * @return certs + */ + default Certificate[] getEvidenceBundleCerts() { + return null; + } +} diff --git a/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientContext.java b/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientContext.java index 06b58b6e..f9893541 100644 --- a/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientContext.java +++ b/src/main/java/com/siemens/pki/cmpclientcomponent/configuration/ClientContext.java @@ -23,6 +23,16 @@ */ public interface ClientContext { + /** + * get remote attestation specific configuration + * @return remote attestation specific configuration + * or null is no remote attestation shall + * be used + */ + default ClientAttestationContext getAttestationContext() { + return null; + } + /** * get enrollment specific configuration * diff --git a/src/main/java/com/siemens/pki/cmpclientcomponent/main/ClientRequestHandler.java b/src/main/java/com/siemens/pki/cmpclientcomponent/main/ClientRequestHandler.java index 3bc3ab93..cf46beae 100644 --- a/src/main/java/com/siemens/pki/cmpclientcomponent/main/ClientRequestHandler.java +++ b/src/main/java/com/siemens/pki/cmpclientcomponent/main/ClientRequestHandler.java @@ -144,7 +144,7 @@ private void validateResponse(final PKIMessage response) throws BaseCmpException private final ValidatorAndProtector nestedValidatorAndProtector; /** - * @param certProfile certificate profile to be used for enrollment. + * @param certProfile certificate profile to be used for enrollment or * null if no certificate profile * should be used. * @@ -184,6 +184,21 @@ PKIMessage buildFurtherRequest(final PKIMessage formerResponse, final PKIBody re false); } + PKIMessage buildFurtherRequest( + final PKIMessage formerResponse, + final PKIBody requestBody, + final boolean withImplicitConfirm, + final int pvno) + throws Exception { + final PKIHeader formerResponseHeader = formerResponse.getHeader(); + return buildRequest( + requestBody, + formerResponseHeader.getTransactionID(), + formerResponseHeader.getSenderNonce(), + pvno, + withImplicitConfirm); + } + PKIMessage buildInitialRequest(final PKIBody requestBody, final boolean withImplicitConfirm) throws Exception { return buildInitialRequest(requestBody, withImplicitConfirm, DEFAULT_PVNO); } @@ -297,6 +312,10 @@ PKIBody sendReceiveInitialBody(final PKIBody body, final boolean withImplicitCon .getBody(); } + PKIMessage sendReceiveInitialMessage(final PKIBody body) throws Exception { + return sendReceiveValidateMessage(buildInitialRequest(body, false), body.getType()); + } + PKIMessage sendReceiveValidateMessage(PKIMessage request, final int firstRequestType) throws Exception { if (nestedValidatorAndProtector != null) { request = nestedValidatorAndProtector diff --git a/src/main/java/com/siemens/pki/cmpclientcomponent/main/CmpClient.java b/src/main/java/com/siemens/pki/cmpclientcomponent/main/CmpClient.java index 9d6b446c..965e8b8c 100644 --- a/src/main/java/com/siemens/pki/cmpclientcomponent/main/CmpClient.java +++ b/src/main/java/com/siemens/pki/cmpclientcomponent/main/CmpClient.java @@ -20,8 +20,10 @@ import static com.siemens.pki.cmpracomponent.util.NullUtil.defaultIfNull; import static com.siemens.pki.cmpracomponent.util.NullUtil.ifNotNull; +import com.siemens.pki.cmpclientcomponent.configuration.ClientAttestationContext; import com.siemens.pki.cmpclientcomponent.configuration.ClientContext; import com.siemens.pki.cmpclientcomponent.configuration.EnrollmentContext; +import com.siemens.pki.cmpclientcomponent.configuration.EnrollmentContext.TemplateExtension; import com.siemens.pki.cmpclientcomponent.configuration.RevocationContext; import com.siemens.pki.cmpracomponent.configuration.CmpMessageInterface; import com.siemens.pki.cmpracomponent.configuration.CrlUpdateRetrievalHandler; @@ -40,6 +42,13 @@ import com.siemens.pki.cmpracomponent.protection.ProtectionProvider; import com.siemens.pki.cmpracomponent.protection.SignatureBasedProtection; import com.siemens.pki.cmpracomponent.util.MessageDumper; +import com.siemens.pki.verifieradapter.asn1.AttestationObjectIdentifiers; +import com.siemens.pki.verifieradapter.asn1.EvidenceBundle; +import com.siemens.pki.verifieradapter.asn1.EvidenceStatement; +import com.siemens.pki.verifieradapter.asn1.NonceRequestValue; +import com.siemens.pki.verifieradapter.asn1.NonceRequestValue.NonceRequest; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue.NonceResponse; import java.io.IOException; import java.security.KeyPair; import java.security.PrivateKey; @@ -146,7 +155,7 @@ public interface EnrollmentResult { /** * ctor * - * @param certProfile certificate profile to be used for enrollment. + * @param certProfile certificate profile to be used for enrollment or * null if no certificate profile * should be used. * @@ -409,6 +418,51 @@ private boolean grantsImplicitConfirm(final PKIMessage msg) { public EnrollmentResult invokeEnrollment() { try { + PKIMessage ratNonceResponse = null; + EvidenceBundle evidenceBundle = null; + final ClientAttestationContext attestationContext = clientContext.getAttestationContext(); + if (attestationContext != null) { + NonceRequest nonceRequest = new NonceRequest( + attestationContext.getNonceRequestLen(), + attestationContext.getNonceRequestType(), + attestationContext.getNonceRequestHint(), + attestationContext.getNonceRequestVendorextension()); + NonceRequestValue nonceRequestValue = new NonceRequestValue(new NonceRequest[] {nonceRequest}); + ratNonceResponse = requestHandler.sendReceiveInitialMessage(new PKIBody( + PKIBody.TYPE_GEN_MSG, + new GenMsgContent(new InfoTypeAndValue( + AttestationObjectIdentifiers.id_it_NonceRequest, nonceRequestValue)))); + final PKIBody ratNonceResponseBody = ratNonceResponse.getBody(); + if (ratNonceResponseBody.getType() != PKIBody.TYPE_GEN_REP) { + logUnexpectedResponse(ratNonceResponseBody); + return null; + } + + final GenRepContent content = (GenRepContent) ratNonceResponseBody.getContent(); + final InfoTypeAndValue[] itav = content.toInfoTypeAndValueArray(); + if (itav != null) { + for (final InfoTypeAndValue aktitav : itav) { + if (AttestationObjectIdentifiers.id_it_NonceResponse.equals(aktitav.getInfoType())) { + final ASN1Encodable infoValue = aktitav.getInfoValue(); + if (infoValue == null) { + LOGGER.error("no RAT nonce received"); + return null; + } + final NonceResponseValue ratNonce = NonceResponseValue.getInstance(infoValue); + NonceResponse[] nonceResponses = ratNonce.getNonceResponse(); + EvidenceStatement[] evidenceStatements = new EvidenceStatement[nonceResponses.length]; + for (int i = 0; i < nonceResponses.length; i++) { + NonceResponse aktnonce = nonceResponses[i]; + evidenceStatements[i] = EvidenceStatement.getInstance( + attestationContext.getEvidenceStatement(aktnonce.getEncoded())); + } + evidenceBundle = + new EvidenceBundle(evidenceStatements, attestationContext.getEvidenceBundleCerts()); + break; + } + } + } + } final EnrollmentContext enrollmentContext = clientContext.getEnrollmentContext(); final KeyPair certificateKeypair = enrollmentContext.getCertificateKeypair(); @@ -459,16 +513,25 @@ public EnrollmentResult invokeEnrollment() { case PKIBody.TYPE_CERT_REQ: case PKIBody.TYPE_INIT_REQ: { final String subject = enrollmentContext.getSubject(); - final Extension[] arrayOfExtensions = - ifNotNull(enrollmentContext.getExtensions(), exts -> exts.stream() - .map(ext -> new Extension( - new ASN1ObjectIdentifier(ext.getId()), ext.isCritical(), ext.getValue())) - .toArray(Extension[]::new)); - final Extensions extensions = ifNotNull(arrayOfExtensions, Extensions::new); + final List extensions = new ArrayList<>(); + + final List extensionsFromConfig = enrollmentContext.getExtensions(); + if (extensionsFromConfig != null) { + extensionsFromConfig.stream() + .map(ext -> new Extension( + new ASN1ObjectIdentifier(ext.getId()), ext.isCritical(), ext.getValue())) + .forEach(extensions::add); + } + if (evidenceBundle != null) { + extensions.add( + Extension.create(AttestationObjectIdentifiers.id_aa_evidence, false, evidenceBundle)); + } final CertTemplateBuilder ctb = new CertTemplateBuilder() .setSubject(ifNotNull(subject, X500Name::new)) - .setPublicKey(enrolledPublicKeyInfo) - .setExtensions(extensions); + .setPublicKey(enrolledPublicKeyInfo); + if (!extensions.isEmpty()) { + ctb.setExtensions(new Extensions(extensions.toArray(new Extension[extensions.size()]))); + } requestBody = PkiMessageGenerator.generateIrCrKurBody( enrollmentType, ctb.build(), null, enrolledPrivateKey); pvno = enrolledPrivateKey == null ? PKIHeader.CMP_2021 : PKIHeader.CMP_2000; @@ -478,9 +541,13 @@ public EnrollmentResult invokeEnrollment() { LOGGER.error("EnrollmentType must be 0(ir), 2(cr), 7(kur) or 4(p10cr)"); return null; } - final PKIMessage responseMessage = requestHandler.sendReceiveValidateMessage( - requestHandler.buildInitialRequest(requestBody, enrollmentContext.getRequestImplictConfirm(), pvno), - enrollmentType); + final PKIMessage enrollmentRequestMessage = ratNonceResponse == null + ? requestHandler.buildInitialRequest( + requestBody, enrollmentContext.getRequestImplictConfirm(), pvno) + : requestHandler.buildFurtherRequest( + ratNonceResponse, requestBody, enrollmentContext.getRequestImplictConfirm(), pvno); + final PKIMessage responseMessage = + requestHandler.sendReceiveValidateMessage(enrollmentRequestMessage, enrollmentType); final PKIBody responseBody = responseMessage.getBody(); final int responseMessageType = responseBody.getType(); if (enrollmentType == PKIBody.TYPE_P10_CERT_REQ) { diff --git a/src/main/java/com/siemens/pki/cmpracomponent/configuration/Configuration.java b/src/main/java/com/siemens/pki/cmpracomponent/configuration/Configuration.java index a48f2e75..a8aab44e 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/configuration/Configuration.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/configuration/Configuration.java @@ -89,6 +89,19 @@ public interface Configuration { */ InventoryInterface getInventory(String certProfile, int bodyType); + /** + * optionally access function to external remote attestation verify adapter + * + * @param certProfile certificate profile extracted from the CMP request header + * generalInfo field or null if no certificate + * profile was specified + * @param bodyType request/response PKI Message Body type + * @return external RatVerifierAdapter or null + */ + default RatVerifierAdapter getVerifierAdapter(String certProfile, int bodyType) { + return null; + } + /** * provide a persistence implementation * diff --git a/src/main/java/com/siemens/pki/cmpracomponent/configuration/CrlUpdateRetrievalHandler.java b/src/main/java/com/siemens/pki/cmpracomponent/configuration/CrlUpdateRetrievalHandler.java index b6255105..fe606076 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/configuration/CrlUpdateRetrievalHandler.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/configuration/CrlUpdateRetrievalHandler.java @@ -37,7 +37,7 @@ public interface CrlUpdateRetrievalHandler extends SupportMessageHandlerInterfac * null if absent in request * @param issuer issuer from CRLSource or null * if absent in request - * @param thisUpdate thisUpdate time from CRLStatus in request + * @param thisUpdate thisUpdate time from CRLStatus in request or * null if absent in request * @return CRLs to be returned or null if the returned infoValue * should be absent diff --git a/src/main/java/com/siemens/pki/cmpracomponent/configuration/GetFreshRatNonceHandler.java b/src/main/java/com/siemens/pki/cmpracomponent/configuration/GetFreshRatNonceHandler.java new file mode 100644 index 00000000..4a110bc5 --- /dev/null +++ b/src/main/java/com/siemens/pki/cmpracomponent/configuration/GetFreshRatNonceHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.cmpracomponent.configuration; + +/** + * support message handler supporting Remote Attestation Procedures nonce genm + * requests, see https://datatracker.ietf.org/doc/draft-ietf-rats-reference-interaction-models/ , + * and https://github.com/veraison/docs/blob/main/api/challenge-response/README.md + */ +public interface GetFreshRatNonceHandler extends SupportMessageHandlerInterface { + // up to now only a placeholder +} diff --git a/src/main/java/com/siemens/pki/cmpracomponent/configuration/RatVerifierAdapter.java b/src/main/java/com/siemens/pki/cmpracomponent/configuration/RatVerifierAdapter.java new file mode 100644 index 00000000..7cb76342 --- /dev/null +++ b/src/main/java/com/siemens/pki/cmpracomponent/configuration/RatVerifierAdapter.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.cmpracomponent.configuration; + +import com.siemens.pki.verifieradapter.asn1.AttestationResult; +import com.siemens.pki.verifieradapter.asn1.EvidenceStatement; +import java.math.BigInteger; + +/** + * adapter to remote attestation verfier + */ +public interface RatVerifierAdapter { + + /** + * turn evidence into verification result + * @param transactionId current CMP transactionId, used to map related calls to getFreshRatNonce and processRatVerification + * @param evidence evidence provided by EE as DER encoded {@link EvidenceStatement} + * @return verification result as DER encoded {@link AttestationResult} + */ + byte[] processRatVerification(byte[] transactionId, byte[] evidence); + + interface NonceResponseRet { + + /** + * returns the nonce of length len provided by the Verifier indicated with hint + * @return nonce + */ + byte[] getNonce(); + + /** + * indicates how long in seconds the Verifier considers the nonce valid + * @return time in seconds or null + */ + default Integer getExpiry() { + return null; + } + + /** + * indicates which Verifier to request a nonce from + * @return hint or null + */ + default String getHint() { + return null; + } + + /** + * indicates which Evidence type to request a nonce for + * @return OID or null + */ + default String getType() { + return null; + } + + /** + * Siemens proprietary extension to carry additional data + * @return additional data or null + */ + default byte[] getVendorextension() { + return null; + } + } + + /** + * Generate nonce + * @param transactionId current CMP transactionId, used to map related calls to getFreshRatNonce and processRatVerification + * @param len the required length of the requested nonce, maybe null + * @param type indicates which Evidence type to request a nonce for, OID as string or null + * @param hint indicates which Verifier to request a nonce from, maybe null + * @param vendorextension additional Siemens proprietary data, maybe null + * @param encodedNonceRequest encoded NonceRequest containing len, type and hint + * @return fresh BER encoded NonceResponse + */ + NonceResponseRet generateNonce( + byte[] transactionId, + BigInteger len, + String type, + String hint, + byte[] vendorextension, + byte[] encodedNonceRequest); +} diff --git a/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/RaDownstream.java b/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/RaDownstream.java index 755c55e5..8011415a 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/RaDownstream.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/RaDownstream.java @@ -26,6 +26,7 @@ import com.siemens.pki.cmpracomponent.configuration.CredentialContext; import com.siemens.pki.cmpracomponent.configuration.InventoryInterface; import com.siemens.pki.cmpracomponent.configuration.NestedEndpointContext; +import com.siemens.pki.cmpracomponent.configuration.RatVerifierAdapter; import com.siemens.pki.cmpracomponent.configuration.SignatureCredentialContext; import com.siemens.pki.cmpracomponent.cryptoservices.AlgorithmHelper; import com.siemens.pki.cmpracomponent.cryptoservices.CertUtility; @@ -52,6 +53,11 @@ import com.siemens.pki.cmpracomponent.util.ConfigLogger; import com.siemens.pki.cmpracomponent.util.MessageDumper; import com.siemens.pki.cmpracomponent.util.NullUtil.ExFunction; +import com.siemens.pki.verifieradapter.asn1.AttestationObjectIdentifiers; +import com.siemens.pki.verifieradapter.asn1.AttestationResult; +import com.siemens.pki.verifieradapter.asn1.AttestationResultBundle; +import com.siemens.pki.verifieradapter.asn1.EvidenceBundle; +import com.siemens.pki.verifieradapter.asn1.EvidenceStatement; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; @@ -66,6 +72,7 @@ import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; @@ -94,6 +101,8 @@ import org.bouncycastle.asn1.pkcs.CertificationRequest; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; @@ -194,7 +203,7 @@ protected CmsEncryptorBase buildEncryptor( */ private PKIMessage handleCrmfCertificateRequest( final PKIMessage incomingCertificateRequest, final PersistencyContext persistencyContext) - throws BaseCmpException, GeneralSecurityException, IOException { + throws BaseCmpException, GeneralSecurityException, IOException, InterruptedException { final PKIBody requestBody = incomingCertificateRequest.getBody(); final PKIBody body = requestBody; @@ -204,6 +213,38 @@ private PKIMessage handleCrmfCertificateRequest( CertRequest certRequest = certReqMsg.getCertReq(); CertTemplate certTemplate = certRequest.getCertTemplate(); + // process RAT verification + final RatVerifierAdapter verifyAdapter = ConfigLogger.logOptional( + INTERFACE_NAME, + "Configuration.getVerifierAdapter", + config::getVerifierAdapter, + persistencyContext.getCertProfile(), + requestBodyType); + if (verifyAdapter != null) { + final Extensions extensions = certTemplate.getExtensions(); + final Extension ratExtension = + processRatVerification(verifyAdapter, persistencyContext.getTransactionId(), extensions); + if (ratExtension != null) { + final Extensions newExtensions = new Extensions(Stream.concat( + Arrays.stream(extensions.getExtensionOIDs()) + .filter(oid -> !oid.equals(AttestationObjectIdentifiers.id_aa_evidence)) + .map(extensions::getExtension), + Arrays.asList(ratExtension).stream()) + .toArray(Extension[]::new)); + certTemplate = new CertTemplateBuilder() + .setVersion(certTemplate.getVersion()) + .setSerialNumber(certTemplate.getSerialNumber()) + .setSigningAlg(certTemplate.getSigningAlg()) + .setIssuer(certTemplate.getIssuer()) + .setValidity(certTemplate.getValidity()) + .setSubject(certTemplate.getSubject()) + .setPublicKey(certTemplate.getPublicKey()) + .setExtensions(newExtensions) + .build(); + certRequest = new CertRequest(0, certTemplate, certRequest.getControls()); + } + } + // check request against inventory final InventoryInterface inventory = ConfigLogger.logOptional( INTERFACE_NAME, @@ -675,6 +716,8 @@ private PKIMessage handleValidatedRequest(final PKIMessage incomingRequest, fina preprocessedRequest = handleCrmfCertificateRequest(incomingRequest, persistencyContext); } catch (final BaseCmpException ex) { throw ex; + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); } catch (final Exception ex) { throw new CmpEnrollmentException( incomingRequest.getBody().getType(), @@ -692,9 +735,11 @@ private PKIMessage handleValidatedRequest(final PKIMessage incomingRequest, fina case PKIBody.TYPE_GEN_MSG: // try to handle locally persistencyContext.setRequestType(incomingRequest.getBody().getType()); - final PKIMessage genmResponse = - new ServiceImplementation(config).handleValidatedInputMessage(incomingRequest, messageContext); + persistencyContext.trackMessage(preprocessedRequest); + final PKIMessage genmResponse = new ServiceImplementation(config, persistencyContext) + .handleValidatedInputMessage(incomingRequest, messageContext); if (genmResponse != null) { + persistencyContext.trackMessage(genmResponse); return genmResponse; } break; @@ -887,4 +932,27 @@ private PKIMessage processCertResponse( "could not properly process certificate response: " + ex); } } + + private Extension processRatVerification( + final RatVerifierAdapter verifyAdapter, final byte[] transactionId, Extensions extensions) + throws IOException { + if (extensions == null) { + return null; + } + final Extension evidenceItav = extensions.getExtension(AttestationObjectIdentifiers.id_aa_evidence); + if (evidenceItav == null) { + return null; + } + EvidenceBundle evidenceBundle = EvidenceBundle.getInstance(evidenceItav.getParsedValue()); + EvidenceStatement[] evidences = evidenceBundle.getEvidences(); + AttestationResult[] attestationResults = new AttestationResult[evidences.length]; + for (int i = 0; i < evidences.length; i++) { + attestationResults[i] = AttestationResult.getInstance( + verifyAdapter.processRatVerification(transactionId, evidences[i].getEncoded())); + } + return Extension.create( + AttestationObjectIdentifiers.id_aa_ar, + false, + new AttestationResultBundle(attestationResults, evidenceBundle.getCerts())); + } } diff --git a/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/ServiceImplementation.java b/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/ServiceImplementation.java index bb2b7328..a95c8085 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/ServiceImplementation.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/msgprocessing/ServiceImplementation.java @@ -23,8 +23,11 @@ import com.siemens.pki.cmpracomponent.configuration.CrlUpdateRetrievalHandler; import com.siemens.pki.cmpracomponent.configuration.GetCaCertificatesHandler; import com.siemens.pki.cmpracomponent.configuration.GetCertificateRequestTemplateHandler; +import com.siemens.pki.cmpracomponent.configuration.GetFreshRatNonceHandler; import com.siemens.pki.cmpracomponent.configuration.GetRootCaCertificateUpdateHandler; import com.siemens.pki.cmpracomponent.configuration.GetRootCaCertificateUpdateHandler.RootCaCertificateUpdateResponse; +import com.siemens.pki.cmpracomponent.configuration.RatVerifierAdapter; +import com.siemens.pki.cmpracomponent.configuration.RatVerifierAdapter.NonceResponseRet; import com.siemens.pki.cmpracomponent.configuration.SupportMessageHandlerInterface; import com.siemens.pki.cmpracomponent.cryptoservices.CertUtility; import com.siemens.pki.cmpracomponent.msggeneration.MsgOutputProtector; @@ -33,6 +36,11 @@ import com.siemens.pki.cmpracomponent.msgvalidation.MessageContext; import com.siemens.pki.cmpracomponent.persistency.PersistencyContext; import com.siemens.pki.cmpracomponent.util.ConfigLogger; +import com.siemens.pki.verifieradapter.asn1.AttestationObjectIdentifiers; +import com.siemens.pki.verifieradapter.asn1.NonceRequestValue; +import com.siemens.pki.verifieradapter.asn1.NonceRequestValue.NonceRequest; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue.NonceResponse; import java.io.IOException; import java.security.cert.CRLException; import java.security.cert.CertificateException; @@ -42,9 +50,12 @@ import java.util.List; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1UTF8String; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.cmp.CMPCertificate; import org.bouncycastle.asn1.cmp.CMPObjectIdentifiers; @@ -72,13 +83,15 @@ class ServiceImplementation { private static final String INTERFACE_NAME = "GENM service"; private final Configuration config; + private final PersistencyContext persistencyContext; /** * @param config specific configuration * @throws Exception in case of error */ - ServiceImplementation(final Configuration config) { + ServiceImplementation(final Configuration config, PersistencyContext persistencyContext) { this.config = config; + this.persistencyContext = persistencyContext; } private String[] generalNamesToStrings(final GeneralNames generalNames) { @@ -254,6 +267,8 @@ protected PKIMessage handleValidatedInputMessage(final PKIMessage msg, final Mes body = handleGetRootCaCertificateUpdate(itav, (GetRootCaCertificateUpdateHandler) messageHandler); } else if (messageHandler instanceof CrlUpdateRetrievalHandler) { body = handleCrlUpdateRetrieval(itav, (CrlUpdateRetrievalHandler) messageHandler); + } else if (messageHandler instanceof GetFreshRatNonceHandler) { + body = handleGetFreshRatNonce(msg, itav); } else { throw new CmpProcessingException(INTERFACE_NAME, PKIFailureInfo.systemFailure, "internal error"); } @@ -277,4 +292,37 @@ protected PKIMessage handleValidatedInputMessage(final PKIMessage msg, final Mes throw new CmpProcessingException(INTERFACE_NAME, e); } } + + private PKIBody handleGetFreshRatNonce(final PKIMessage msg, final InfoTypeAndValue itav) + throws CmpProcessingException, RuntimeException, IOException { + final RatVerifierAdapter verifyAdapter = ConfigLogger.logOptional( + INTERFACE_NAME, + "Configuration.getVerifierAdapter", + config::getVerifierAdapter, + persistencyContext.getCertProfile(), + PKIBody.TYPE_GEN_MSG); + if (verifyAdapter == null) { + throw new CmpProcessingException(INTERFACE_NAME, PKIFailureInfo.systemFailure, "RAT not configured"); + } + NonceRequestValue nonceRequestValue = NonceRequestValue.getInstance(itav.getInfoValue()); + NonceRequest[] nonceRequests = nonceRequestValue.getNonceRequests(); + NonceResponse[] responses = new NonceResponse[nonceRequests.length]; + for (int i = 0; i < nonceRequests.length; i++) { + NonceRequest aktRequest = nonceRequests[i]; + NonceResponseRet ret = verifyAdapter.generateNonce( + msg.getHeader().getTransactionID().getOctets(), + ifNotNull(aktRequest.getLen(), ASN1Integer::getValue), + ifNotNull(aktRequest.getType(), ASN1ObjectIdentifier::getId), + ifNotNull(aktRequest.getHint(), ASN1UTF8String::getString), + ifNotNull(aktRequest.getVendorextension(), ASN1OctetString::getOctets), + aktRequest.getEncoded()); + responses[i] = new NonceResponse( + ret.getNonce(), ret.getExpiry(), ret.getType(), ret.getHint(), ret.getVendorextension()); + } + persistencyContext.markAsPreparingGenm(); + return new PKIBody( + PKIBody.TYPE_GEN_REP, + new GenRepContent(new InfoTypeAndValue( + AttestationObjectIdentifiers.id_it_NonceResponse, new NonceResponseValue(responses)))); + } } diff --git a/src/main/java/com/siemens/pki/cmpracomponent/persistency/PersistencyContext.java b/src/main/java/com/siemens/pki/cmpracomponent/persistency/PersistencyContext.java index 7adabb7c..3936d92e 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/persistency/PersistencyContext.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/persistency/PersistencyContext.java @@ -52,6 +52,7 @@ public class PersistencyContext { private CMPCertificate enrolledCertificate; private boolean implicitConfirmGranted; private byte[] requestedPublicKey; + private boolean markedAsPreparingGenm; @JsonIgnore private List issuingChain; @@ -216,6 +217,12 @@ public byte[] getTransactionId() { public boolean isImplicitConfirmGranted() { return implicitConfirmGranted; } + /** + * mark the currently processed GENM as preliminary for a remaining transaction + */ + public void markAsPreparingGenm() { + markedAsPreparingGenm = true; + } /** * store already sent extra certs in case of compression @@ -374,4 +381,8 @@ public void setRespondedCertMustBeEncrypted() { public boolean isRespondedCertMustBeEncrypted() { return respondedCertMustBeEncrypted; } + + boolean isMarkedAsPreparingGenm() { + return markedAsPreparingGenm; + } } diff --git a/src/main/java/com/siemens/pki/cmpracomponent/persistency/TransactionStateTracker.java b/src/main/java/com/siemens/pki/cmpracomponent/persistency/TransactionStateTracker.java index 1ffa7b0e..eb336f01 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/persistency/TransactionStateTracker.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/persistency/TransactionStateTracker.java @@ -148,19 +148,6 @@ private boolean isCertResponse(final PKIMessage msg) { } } - private boolean isCertResponseWithWaitingIndication(final PKIMessage msg) { - try { - return ((CertRepMessage) msg.getBody().getContent()) - .getResponse()[0] - .getStatus() - .getStatus() - .intValue() - == PKIStatus.WAITING; - } catch (final Exception ex) { - return false; - } - } - private boolean isConfirmConfirm(final PKIMessage msg) { return msg.getBody().getType() == PKIBody.TYPE_CONFIRM; } @@ -378,7 +365,7 @@ public void trackMessage(final PKIMessage message) throws BaseCmpException, IOEx PKIFailureInfo.badMessageCheck, "request was not answered by cert response for " + MessageDumper.msgAsShortString(message)); } - if (isCertResponseWithWaitingIndication(message)) { + if (isWaitingIndication(message)) { persistencyContext.setLastTransactionState(LastTransactionState.CERTIFICATE_POLLING); return; } @@ -447,7 +434,8 @@ public void trackMessage(final PKIMessage message) throws BaseCmpException, IOEx throw new CmpValidationException( INTERFACE_NAME, PKIFailureInfo.transactionIdInUse, - "transaction in wrong state for " + MessageDumper.msgAsShortString(message)); + "transaction in wrong state " + persistencyContext.getLastTransactionState() + " for " + + MessageDumper.msgAsShortString(message)); } persistencyContext.setLastTransactionState(LastTransactionState.REVOCATION_CONFIRMED); return; @@ -459,7 +447,8 @@ public void trackMessage(final PKIMessage message) throws BaseCmpException, IOEx throw new CmpValidationException( INTERFACE_NAME, PKIFailureInfo.transactionIdInUse, - "transaction in wrong state for " + MessageDumper.msgAsShortString(message)); + "transaction in wrong state " + persistencyContext.getLastTransactionState() + " for " + + MessageDumper.msgAsShortString(message)); } persistencyContext.setLastTransactionState(LastTransactionState.REVOCATION_CONFIRMED); return; @@ -472,9 +461,14 @@ public void trackMessage(final PKIMessage message) throws BaseCmpException, IOEx throw new CmpValidationException( INTERFACE_NAME, PKIFailureInfo.transactionIdInUse, - "transaction in wrong state for " + MessageDumper.msgAsShortString(message)); + "transaction in wrong state " + persistencyContext.getLastTransactionState() + " for " + + MessageDumper.msgAsShortString(message)); + } + if (persistencyContext.isMarkedAsPreparingGenm()) { + persistencyContext.setLastTransactionState(LastTransactionState.INITIAL_STATE); + } else { + persistencyContext.setLastTransactionState(LastTransactionState.GENREP_RETURNED); } - persistencyContext.setLastTransactionState(LastTransactionState.GENREP_RETURNED); return; case GEN_POLLING: if (isPollRequest(message) || isPollResponse(message)) { @@ -484,7 +478,8 @@ public void trackMessage(final PKIMessage message) throws BaseCmpException, IOEx throw new CmpValidationException( INTERFACE_NAME, PKIFailureInfo.transactionIdInUse, - "transaction in wrong state for " + MessageDumper.msgAsShortString(message)); + "transaction in wrong state " + persistencyContext.getLastTransactionState() + " for " + + MessageDumper.msgAsShortString(message)); } persistencyContext.setLastTransactionState(LastTransactionState.GENREP_RETURNED); return; @@ -492,7 +487,7 @@ public void trackMessage(final PKIMessage message) throws BaseCmpException, IOEx throw new CmpValidationException( INTERFACE_NAME, PKIFailureInfo.transactionIdInUse, - "transaction in wrong state (" + persistencyContext.getLastTransactionState() + ") for " + "transaction in wrong state " + persistencyContext.getLastTransactionState() + " for " + MessageDumper.msgAsShortString(message)); } } diff --git a/src/main/java/com/siemens/pki/cmpracomponent/util/MessageDumper.java b/src/main/java/com/siemens/pki/cmpracomponent/util/MessageDumper.java index 60072958..427e33e0 100644 --- a/src/main/java/com/siemens/pki/cmpracomponent/util/MessageDumper.java +++ b/src/main/java/com/siemens/pki/cmpracomponent/util/MessageDumper.java @@ -19,6 +19,7 @@ import static com.siemens.pki.cmpracomponent.util.NullUtil.ifNotNull; +import com.siemens.pki.verifieradapter.asn1.AttestationObjectIdentifiers; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -464,7 +465,8 @@ private static synchronized void initNameOidMaps() { PQCObjectIdentifiers.class, org.bouncycastle.asn1.x509.Extension.class, EdECObjectIdentifiers.class, - CMPObjectIdentifiers.class)) { + CMPObjectIdentifiers.class, + AttestationObjectIdentifiers.class)) { for (final Field aktField : aktClass.getFields()) { if (aktField.getType().equals(ASN1ObjectIdentifier.class) && (aktField.getModifiers() & Modifier.STATIC) != 0) { diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationObjectIdentifiers.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationObjectIdentifiers.java new file mode 100644 index 00000000..6b1cd428 --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationObjectIdentifiers.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.verifieradapter.asn1; + +/** + * OID definition from + * https://datatracker.ietf.org/doc/draft-ietf-lamps-attestation-freshness/ and + * https://datatracker.ietf.org/doc/draft-ietf-lamps-csr-attestation/ + */ +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; + +/** + * OIDs from from https://datatracker.ietf.org/doc/draft-ietf-lamps-attestation-freshness/ and + * https://datatracker.ietf.org/doc/draft-ietf-lamps-csr-attestation/ + */ +public interface AttestationObjectIdentifiers { + + /** + * Branch for attestation statement types + */ + /** RFC 4120: id-it: PKIX.4 = 1.3.6.1.5.5.7.4 */ + ASN1ObjectIdentifier id_it = X509ObjectIdentifiers.id_pkix.branch("4"); + + /** + * from https://datatracker.ietf.org/doc/draft-ietf-lamps-attestation-freshness/ + * TODO update to current state of the draft + */ + String TBD1 = "99"; + + String TBD2 = "100"; + String TBD3 = "101"; + + ASN1ObjectIdentifier id_it_NonceRequest = id_it.branch(TBD1); + ASN1ObjectIdentifier id_it_NonceResponse = id_it.branch(TBD2); + + /** + * from https://datatracker.ietf.org/doc/draft-ietf-lamps-csr-attestation/ + */ + ASN1ObjectIdentifier id_aa_evidence = PKCSObjectIdentifiers.id_aa.branch("59"); + + ASN1ObjectIdentifier id_aa_ar = PKCSObjectIdentifiers.id_aa.branch(TBD3); +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationResult.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationResult.java new file mode 100644 index 00000000..d6530357 --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationResult.java @@ -0,0 +1,79 @@ +package com.siemens.pki.verifieradapter.asn1; + +/* + * Copyright (c) 2023 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; + +/** + * {@code + * AttestationResult ::= SEQUENCE { + * type ATTESTATION-RESULT.&id({AttestationResultSet}), + * stmt ATTESTATION-RESULT.&Type({AttestationResultSet}{@type}), + * } + * } + */ +public class AttestationResult extends ASN1Object { + public static AttestationResult getInstance(Object o) { + if (o instanceof AttestationResult) { + return (AttestationResult) o; + } + + if (o != null) { + return new AttestationResult(ASN1Sequence.getInstance(o)); + } + + return null; + } + + private final ASN1ObjectIdentifier type; + + private final ASN1Encodable stmt; + + public AttestationResult(ASN1ObjectIdentifier type, ASN1Encodable stmt) { + this.type = type; + this.stmt = stmt; + } + + private AttestationResult(ASN1Sequence seq) { + type = ASN1ObjectIdentifier.getInstance(seq.getObjectAt(0)); + stmt = seq.getObjectAt(1); + } + + public ASN1Encodable getStmt() { + return stmt; + } + + public ASN1ObjectIdentifier getType() { + return type; + } + + @Override + public ASN1Primitive toASN1Primitive() { + final ASN1EncodableVector v = new ASN1EncodableVector(2); + v.add(type); + v.add(stmt); + return new DERSequence(v); + } +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationResultBundle.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationResultBundle.java new file mode 100644 index 00000000..c3ba7746 --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/AttestationResultBundle.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.verifieradapter.asn1; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.Certificate; + +/** + * {@code + * AttestationResultBundle ::= SEQUENCE { + * results SEQUENCE SIZE (1..MAX) OF AttestationResult, + * certs SEQUENCE SIZE (1..MAX) OF CertificateChoices OPTIONAL, + * -- CertificateChoices MUST only contain certificate or other, + * -- see Section 10.2.2 of [RFC5652] + * } + * } + * + */ +public class AttestationResultBundle extends ASN1Object { + + private final ASN1EncodableVector results; + private final ASN1EncodableVector certs; + + private AttestationResultBundle(ASN1Sequence sequence) { + results = new ASN1EncodableVector(); + results.addAll(ASN1Sequence.getInstance(sequence.getObjectAt(0)).toArray()); + if (sequence.size() > 1) { + certs = new ASN1EncodableVector(); + certs.addAll(ASN1Sequence.getInstance(sequence.getObjectAt(1)).toArray()); + } else { + certs = null; + } + } + + public AttestationResultBundle(AttestationResult[] results, Certificate[] certs) { + this.results = new ASN1EncodableVector(); + this.results.addAll(results); + if (certs != null) { + this.certs = new ASN1EncodableVector(); + this.certs.addAll(certs); + } else { + this.certs = null; + } + } + + @Override + public ASN1Primitive toASN1Primitive() { + ASN1EncodableVector ret = new ASN1EncodableVector(); + ret.add(new DERSequence(results)); + if (certs != null) { + ret.add(new DERSequence(certs)); + } + return new DERSequence(ret); + } + + public static AttestationResultBundle getInstance(Object o) { + if (o instanceof AttestationResultBundle) { + return (AttestationResultBundle) o; + } else if (o != null) { + return new AttestationResultBundle(ASN1Sequence.getInstance(o)); + } + return null; + } + + public Certificate[] getCerts() { + if (certs == null) { + return null; + } + Certificate[] ret = new Certificate[certs.size()]; + for (int i = 0; i < certs.size(); i++) { + ret[i] = Certificate.getInstance(certs.get(i)); + } + return ret; + } + + public AttestationResult[] getResults() { + AttestationResult[] ret = new AttestationResult[results.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = AttestationResult.getInstance(results.get(i)); + } + return ret; + } +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/EvidenceBundle.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/EvidenceBundle.java new file mode 100644 index 00000000..adb8f29c --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/EvidenceBundle.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.verifieradapter.asn1; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.Certificate; + +/** + * + * {@code + * EvidenceBundle ::= SEQUENCE { + * evidences SEQUENCE SIZE (1..MAX) OF EvidenceStatement, + * certs SEQUENCE SIZE (1..MAX) OF LimitedCertChoices OPTIONAL + * } + * } + */ +public class EvidenceBundle extends ASN1Object { + + private final ASN1EncodableVector evidences; + private final ASN1EncodableVector certs; + + private EvidenceBundle(ASN1Sequence sequence) { + evidences = new ASN1EncodableVector(); + evidences.addAll(ASN1Sequence.getInstance(sequence.getObjectAt(0)).toArray()); + if (sequence.size() > 1) { + certs = new ASN1EncodableVector(); + certs.addAll(ASN1Sequence.getInstance(sequence.getObjectAt(1)).toArray()); + } else { + certs = null; + } + } + + public EvidenceBundle(EvidenceStatement[] evidences, Certificate[] certs) { + this.evidences = new ASN1EncodableVector(); + this.evidences.addAll(evidences); + if (certs != null) { + this.certs = new ASN1EncodableVector(); + this.certs.addAll(certs); + } else { + this.certs = null; + } + } + + @Override + public ASN1Primitive toASN1Primitive() { + ASN1EncodableVector ret = new ASN1EncodableVector(); + ret.add(new DERSequence(evidences)); + if (certs != null && certs.size() > 0) { + ret.add(new DERSequence(certs)); + } + return new DERSequence(ret); + } + + public static EvidenceBundle getInstance(Object o) { + if (o instanceof EvidenceBundle) { + return (EvidenceBundle) o; + } else if (o != null) { + return new EvidenceBundle(ASN1Sequence.getInstance(o)); + } + return null; + } + + public EvidenceStatement[] getEvidences() { + EvidenceStatement[] ret = new EvidenceStatement[evidences.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = EvidenceStatement.getInstance(evidences.get(i)); + } + return ret; + } + + public Certificate[] getCerts() { + if (certs == null) { + return null; + } + Certificate[] ret = new Certificate[certs.size()]; + for (int i = 0; i < certs.size(); i++) { + ret[i] = Certificate.getInstance(certs.get(i)); + } + return ret; + } +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/EvidenceStatement.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/EvidenceStatement.java new file mode 100644 index 00000000..414ff9cd --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/EvidenceStatement.java @@ -0,0 +1,101 @@ +package com.siemens.pki.verifieradapter.asn1; + +/* + * Copyright (c) 2023 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1IA5String; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; + +/** + * {@code + * EvidenceStatement ::= SEQUENCE { + * type EVIDENCE-STATEMENT.&id({EvidenceStatementSet}), + * stmt EVIDENCE-STATEMENT.&Type({EvidenceStatementSet}{@type}), + * hint IA5String OPTIONAL + * } + * } + */ +public class EvidenceStatement extends ASN1Object { + + public static EvidenceStatement getInstance(Object o) { + if (o instanceof EvidenceStatement) { + return (EvidenceStatement) o; + } + + if (o != null) { + return new EvidenceStatement(ASN1Sequence.getInstance(o)); + } + + return null; + } + + private final ASN1ObjectIdentifier type; + + private final ASN1Encodable stmt; + + private final ASN1IA5String hint; + + public EvidenceStatement(ASN1ObjectIdentifier type, ASN1Encodable stmt, ASN1IA5String hint) { + if (type == null) { + throw new NullPointerException("'type' cannot be null"); + } + + this.type = type; + this.stmt = stmt; + this.hint = hint; + } + + public ASN1IA5String getHint() { + return hint; + } + + private EvidenceStatement(ASN1Sequence seq) { + type = ASN1ObjectIdentifier.getInstance(seq.getObjectAt(0)); + stmt = seq.getObjectAt(1); + if (seq.size() > 2) { + hint = ASN1IA5String.getInstance(seq.getObjectAt(2)); + } else { + hint = null; + } + } + + public ASN1Encodable getStmt() { + return stmt; + } + + public ASN1ObjectIdentifier getType() { + return type; + } + + @Override + public ASN1Primitive toASN1Primitive() { + final ASN1EncodableVector v = new ASN1EncodableVector(3); + v.add(type); + v.add(stmt); + if (hint != null) { + v.add(hint); + } + return new DERSequence(v); + } +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/NonceRequestValue.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/NonceRequestValue.java new file mode 100644 index 00000000..8d1a7f4c --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/NonceRequestValue.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.verifieradapter.asn1; + +import java.math.BigInteger; +import java.util.Enumeration; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1UTF8String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.DERUTF8String; + +/** + * from https://datatracker.ietf.org/doc/draft-ietf-lamps-attestation-freshness/ + * + * {@code + * NonceRequestValue ::= SEQUENCE SIZE (1..MAX) OF NonceRequest + * NonceRequest ::= SEQUENCE { + * len INTEGER OPTIONAL, + * -- indicates the required length of the requested nonce + * type EVIDENCE-STATEMENT.&id({EvidenceStatementSet}) OPTIONAL, + * -- indicates which Evidence type to request a nonce for + * hint UTF8String OPTIONAL + * -- indicates which Verifier to request a nonce from + * vendorextension OCTET STRING OPTIONAL + * -- Siemens proprietary extension to carry additional data + * } + * } + */ +public class NonceRequestValue extends ASN1Object { + + ASN1EncodableVector nonceRequests = new ASN1EncodableVector(); + + public static class NonceRequest extends ASN1Object { + public ASN1Integer getLen() { + return len; + } + + public ASN1ObjectIdentifier getType() { + return type; + } + + public ASN1UTF8String getHint() { + return hint; + } + + public ASN1OctetString getVendorextension() { + return vendorextension; + } + + private ASN1Integer len = null; + private ASN1ObjectIdentifier type = null; + private ASN1UTF8String hint = null; + private ASN1OctetString vendorextension = null; + + public NonceRequest( + ASN1Integer len, ASN1ObjectIdentifier type, ASN1UTF8String hint, ASN1OctetString vendorextension) { + this.len = len; + this.type = type; + this.hint = hint; + this.vendorextension = vendorextension; + } + + public ASN1Primitive toASN1Primitive() { + + ASN1EncodableVector v = new ASN1EncodableVector(4); + + addOptional(v, len); + addOptional(v, type); + addOptional(v, hint); + addOptional(v, vendorextension); + + return new DERSequence(v); + } + + public static NonceRequest getInstance(Object o) { + if (o instanceof NonceRequest) { + return (NonceRequest) o; + } else if (o != null) { + return new NonceRequest(ASN1Sequence.getInstance(o)); + } + + return null; + } + + private NonceRequest(ASN1Sequence seq) { + Enumeration en = seq.getObjects(); + if (!en.hasMoreElements()) { + return; + } + Object next = en.nextElement(); + if (next instanceof ASN1Integer) { + len = ASN1Integer.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + } + if (next instanceof ASN1ObjectIdentifier) { + type = ASN1ObjectIdentifier.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + } + if (next instanceof ASN1UTF8String) { + hint = ASN1UTF8String.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + } + if (next instanceof ASN1OctetString) { + vendorextension = ASN1OctetString.getInstance(next); + } + } + + public NonceRequest( + BigInteger nonceRequestLen, + String nonceRequestType, + String nonceRequestHint, + byte[] nonceRequestVendorextension) { + this( + nonceRequestLen != null ? new ASN1Integer(nonceRequestLen) : null, + nonceRequestType != null ? new ASN1ObjectIdentifier(nonceRequestType) : null, + nonceRequestHint != null ? new DERUTF8String(nonceRequestHint) : null, + nonceRequestVendorextension != null ? new DEROctetString(nonceRequestVendorextension) : null); + } + + private void addOptional(ASN1EncodableVector v, ASN1Encodable obj) { + if (obj != null) { + v.add(obj); + } + } + } + + public NonceRequestValue(NonceRequest[] requests) { + nonceRequests.addAll(requests); + } + + public NonceRequest[] getNonceRequests() { + NonceRequest[] ret = new NonceRequest[nonceRequests.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = NonceRequest.getInstance(nonceRequests.get(i)); + } + return ret; + } + + public NonceRequestValue(ASN1Sequence instance) { + for (int i = 0; i < instance.size(); i++) { + nonceRequests.add(instance.getObjectAt(i)); + } + } + + @Override + public ASN1Primitive toASN1Primitive() { + return new DERSequence(nonceRequests); + } + + public static NonceRequestValue getInstance(Object o) { + if (o instanceof NonceRequestValue) { + return (NonceRequestValue) o; + } else if (o != null) { + return new NonceRequestValue(ASN1Sequence.getInstance(o)); + } + return null; + } +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/NonceResponseValue.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/NonceResponseValue.java new file mode 100644 index 00000000..95adf2ca --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/NonceResponseValue.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.verifieradapter.asn1; + +import java.util.Enumeration; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1UTF8String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.DERUTF8String; + +/** + * from https://datatracker.ietf.org/doc/draft-ietf-lamps-attestation-freshness/ + * + * {@code + * NonceResponseValue ::= SEQUENCE SIZE (1..MAX) OF NonceResponse + * NonceResponse ::= SEQUENCE { + * nonce OCTET STRING, + * -- contains the nonce of length len + * -- provided by the Verifier indicated with hint + * expiry INTEGER OPTIONAL, + * -- indicates how long in seconds the Verifier considers + * -- the nonce valid + * type EVIDENCE-STATEMENT.&id({EvidenceStatementSet}) OPTIONAL, + * -- indicates which Evidence type to request a nonce for + * hint UTF8String OPTIONAL + * -- indicates which Verifier to request a nonce from + * vendorextension OCTET STRING OPTIONAL + * -- Siemens proprietary extension to carry additional data + * } + * } + */ +public class NonceResponseValue extends ASN1Object { + + ASN1EncodableVector nonceResponses = new ASN1EncodableVector(); + + public static class NonceResponse extends ASN1Object { + + private final ASN1OctetString nonce; + private ASN1Integer expiry = null; + private ASN1ObjectIdentifier type = null; + private ASN1UTF8String hint = null; + private ASN1OctetString vendorextension = null; + + public ASN1Primitive toASN1Primitive() { + + ASN1EncodableVector v = new ASN1EncodableVector(4); + + v.add(nonce); + addOptional(v, expiry); + addOptional(v, type); + addOptional(v, hint); + addOptional(v, vendorextension); + + return new DERSequence(v); + } + + public static NonceResponse getInstance(Object o) { + if (o instanceof NonceResponse) { + return (NonceResponse) o; + } else if (o != null) { + return new NonceResponse(ASN1Sequence.getInstance(o)); + } + return null; + } + + private NonceResponse(ASN1Sequence seq) { + Enumeration en = seq.getObjects(); + if (!en.hasMoreElements()) { + throw new IllegalArgumentException("NonceResponse missing nonce value"); + } + Object next = en.nextElement(); + nonce = ASN1OctetString.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + if (next instanceof ASN1Integer) { + expiry = ASN1Integer.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + } + + if (next instanceof ASN1ObjectIdentifier) { + type = ASN1ObjectIdentifier.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + } + if (next instanceof ASN1UTF8String) { + hint = ASN1UTF8String.getInstance(next); + if (!en.hasMoreElements()) { + return; + } + next = en.nextElement(); + } + if (next instanceof ASN1OctetString) { + vendorextension = ASN1OctetString.getInstance(next); + } + } + + public NonceResponse( + ASN1OctetString nonce, + ASN1Integer expiry, + ASN1ObjectIdentifier type, + ASN1UTF8String hint, + ASN1OctetString vendorextension) { + this.nonce = nonce; + this.expiry = expiry; + this.type = type; + this.hint = hint; + this.vendorextension = vendorextension; + } + + public NonceResponse(byte[] nonce, Integer expiry, String type, String hint, byte[] vendorextension) { + this( + nonce != null ? new DEROctetString(nonce) : null, + expiry != null ? new ASN1Integer(expiry) : null, + type != null ? new ASN1ObjectIdentifier(type) : null, + hint != null ? new DERUTF8String(hint) : null, + vendorextension != null ? new DEROctetString(vendorextension) : null); + } + + public ASN1OctetString getNonce() { + return nonce; + } + + public ASN1Integer getExpiry() { + return expiry; + } + + public ASN1ObjectIdentifier getType() { + return type; + } + + public ASN1UTF8String getHint() { + return hint; + } + + public ASN1OctetString getVendorextension() { + return vendorextension; + } + + private void addOptional(ASN1EncodableVector v, ASN1Encodable obj) { + if (obj != null) { + v.add(obj); + } + } + } + + public NonceResponseValue(ASN1Sequence instance) { + for (int i = 0; i < instance.size(); i++) { + nonceResponses.add(instance.getObjectAt(i)); + } + } + + public NonceResponseValue(NonceResponse[] responses) { + nonceResponses.addAll(responses); + } + + public NonceResponse[] getNonceResponse() { + NonceResponse[] ret = new NonceResponse[nonceResponses.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = NonceResponse.getInstance(nonceResponses.get(i)); + } + return ret; + } + + @Override + public ASN1Primitive toASN1Primitive() { + return new DERSequence(nonceResponses); + } + + public static NonceResponseValue getInstance(Object o) { + if (o instanceof NonceResponseValue) { + return (NonceResponseValue) o; + } else if (o != null) { + return new NonceResponseValue(ASN1Sequence.getInstance(o)); + } + return null; + } +} diff --git a/src/main/java/com/siemens/pki/verifieradapter/asn1/package-info.java b/src/main/java/com/siemens/pki/verifieradapter/asn1/package-info.java new file mode 100644 index 00000000..4aaca996 --- /dev/null +++ b/src/main/java/com/siemens/pki/verifieradapter/asn1/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * ASN.1 types used in remote attestation (RAT) + */ +package com.siemens.pki.verifieradapter.asn1; diff --git a/src/test/java/com/siemens/pki/cmpclientcomponent/test/TestCrWithRAT.java b/src/test/java/com/siemens/pki/cmpclientcomponent/test/TestCrWithRAT.java new file mode 100644 index 00000000..d49f027c --- /dev/null +++ b/src/test/java/com/siemens/pki/cmpclientcomponent/test/TestCrWithRAT.java @@ -0,0 +1,583 @@ +/* + * Copyright (c) 2023 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.cmpclientcomponent.test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import com.siemens.pki.cmpclientcomponent.configuration.ClientAttestationContext; +import com.siemens.pki.cmpclientcomponent.configuration.ClientContext; +import com.siemens.pki.cmpclientcomponent.configuration.EnrollmentContext; +import com.siemens.pki.cmpclientcomponent.configuration.RevocationContext; +import com.siemens.pki.cmpclientcomponent.main.CmpClient.EnrollmentResult; +import com.siemens.pki.cmpracomponent.configuration.CheckAndModifyResult; +import com.siemens.pki.cmpracomponent.configuration.CkgContext; +import com.siemens.pki.cmpracomponent.configuration.CmpMessageInterface; +import com.siemens.pki.cmpracomponent.configuration.CmpMessageInterface.ReprotectMode; +import com.siemens.pki.cmpracomponent.configuration.Configuration; +import com.siemens.pki.cmpracomponent.configuration.CredentialContext; +import com.siemens.pki.cmpracomponent.configuration.GetFreshRatNonceHandler; +import com.siemens.pki.cmpracomponent.configuration.InventoryInterface; +import com.siemens.pki.cmpracomponent.configuration.NestedEndpointContext; +import com.siemens.pki.cmpracomponent.configuration.PersistencyInterface; +import com.siemens.pki.cmpracomponent.configuration.RatVerifierAdapter; +import com.siemens.pki.cmpracomponent.configuration.SupportMessageHandlerInterface; +import com.siemens.pki.cmpracomponent.configuration.VerificationContext; +import com.siemens.pki.cmpracomponent.cryptoservices.CertUtility; +import com.siemens.pki.cmpracomponent.persistency.DefaultPersistencyImplementation; +import com.siemens.pki.cmpracomponent.test.framework.ConfigurationFactory; +import com.siemens.pki.cmpracomponent.test.framework.SignatureValidationCredentials; +import com.siemens.pki.cmpracomponent.test.framework.TrustChainAndPrivateKey; +import com.siemens.pki.cmpracomponent.util.MessageDumper; +import com.siemens.pki.verifieradapter.asn1.AttestationObjectIdentifiers; +import com.siemens.pki.verifieradapter.asn1.AttestationResult; +import com.siemens.pki.verifieradapter.asn1.AttestationResultBundle; +import com.siemens.pki.verifieradapter.asn1.EvidenceStatement; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue.NonceResponse; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.x509.Certificate; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestCrWithRAT extends EnrollmentTestcaseBase { + + private static final String UPSTREAM_TRUST_PATH = "credentials/CMP_CA_and_LRA_DOWNSTREAM_Root.pem"; + + private static final Logger LOGGER = LoggerFactory.getLogger(TestCrWithRAT.class); + + private static Configuration buildSignatureBasedDownstreamConfiguration() throws Exception { + final TrustChainAndPrivateKey downstreamCredentials = + new TrustChainAndPrivateKey("credentials/CMP_LRA_DOWNSTREAM_Keystore.p12", "Password".toCharArray()); + final SignatureValidationCredentials downstreamTrust = + new SignatureValidationCredentials("credentials/CMP_EE_Root.pem", null); + final TrustChainAndPrivateKey upstreamCredentials = + new TrustChainAndPrivateKey("credentials/CMP_LRA_UPSTREAM_Keystore.p12", "Password".toCharArray()); + final SignatureValidationCredentials upstreamTrust = + new SignatureValidationCredentials("credentials/CMP_CA_Root.pem", null); + final SignatureValidationCredentials enrollmentTrust = + new SignatureValidationCredentials("credentials/ENROLL_Root.pem", null); + + return buildSimpleRaConfiguration( + downstreamCredentials, + ReprotectMode.keep, + downstreamTrust, + upstreamCredentials, + upstreamTrust, + enrollmentTrust); + } + + private static Configuration buildSimpleRaConfiguration( + final CredentialContext downstreamCredentials, + ReprotectMode reprotectMode, + final VerificationContext downstreamTrust, + final CredentialContext upstreamCredentials, + final VerificationContext upstreamTrust, + final SignatureValidationCredentials enrollmentTrust) { + return new Configuration() { + PersistencyInterface persistency = new DefaultPersistencyImplementation(5000); + + @Override + public CkgContext getCkgConfiguration(final String certProfile, final int bodyType) { + fail(String.format( + "getCkgConfiguration called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType))); + return null; + } + + @Override + public CmpMessageInterface getDownstreamConfiguration(final String certProfile, final int bodyType) { + LOGGER.debug( + "getDownstreamConfiguration called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return new CmpMessageInterface() { + + @Override + public VerificationContext getInputVerification() { + if (certProfile != null) { + switch (certProfile) { + case "certProfileForKur": + case "certProfileForRr": + return enrollmentTrust; + } + } + return downstreamTrust; + } + + @Override + public NestedEndpointContext getNestedEndpointContext() { + return null; + } + + @Override + public CredentialContext getOutputCredentials() { + try { + return downstreamCredentials; + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public ReprotectMode getReprotectMode() { + return reprotectMode; + } + + @Override + public boolean getSuppressRedundantExtraCerts() { + return false; + } + + @Override + public boolean isCacheExtraCerts() { + return false; + } + + @Override + public boolean isMessageTimeDeviationAllowed(final long deviation) { + return true; + } + }; + } + + @Override + public int getDownstreamTimeout(final String certProfile, final int bodyType) { + return 10; + } + + @Override + public VerificationContext getEnrollmentTrust(final String certProfile, final int bodyType) { + LOGGER.debug( + "getEnrollmentTrust called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return enrollmentTrust; + } + + @Override + public boolean getForceRaVerifyOnUpstream(final String certProfile, final int bodyType) { + LOGGER.debug( + "getForceRaVerifyOnUpstream called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return false; + } + + @Override + public RatVerifierAdapter getVerifierAdapter(String certProfile, int bodyType) { + return new RatVerifierAdapter() { + + @Override + public byte[] processRatVerification(byte[] transactionId, byte[] evidence) { + LOGGER.debug( + "processRatVerification called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + + EvidenceStatement evidenceStatement = EvidenceStatement.getInstance(evidence); + assertNotNull(evidenceStatement.getHint()); + assertNotNull(evidenceStatement.getStmt()); + + try { + return new AttestationResult( + evidenceStatement.getType().branch("99"), + new DERUTF8String("attestation result stm")) + .getEncoded(); + } catch (IOException e) { + fail(e.getMessage()); + return null; + } + } + + @Override + public NonceResponseRet generateNonce( + byte[] transactionId, + BigInteger len, + String type, + String hint, + byte[] vendorextension, + byte[] encodedNonceRequest) { + LOGGER.debug( + "generateNonce called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return new NonceResponseRet() { + + @Override + public byte[] getNonce() { + return CertUtility.generateRandomBytes(len.intValue()); + } + + @Override + public Integer getExpiry() { + return 999; + } + + @Override + public String getHint() { + return "responded hint: " + hint; + } + + @Override + public byte[] getVendorextension() { + return vendorextension; + } + + @Override + public String getType() { + return new ASN1ObjectIdentifier("1.7.8.9").getId(); + } + }; + } + }; + } + + @Override + public InventoryInterface getInventory(final String certProfile, final int bodyType) { + LOGGER.debug( + "getInventory called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return new InventoryInterface() { + + @Override + public CheckAndModifyResult checkAndModifyCertRequest( + final byte[] transactionID, + final String requesterDn, + final byte[] certTemplate, + final String requestedSubjectDn, + byte[] pkiMessage) { + LOGGER.debug( + "checkAndModifyCertRequest called with transactionID: {}, requesterDn: {}, requestedSubjectDn: {}", + new BigInteger(transactionID), + requesterDn, + requestedSubjectDn); + return new CheckAndModifyResult() { + + @Override + public byte[] getUpdatedCertTemplate() { + return null; + } + + @Override + public boolean isGranted() { + return true; + } + }; + } + + @Override + public boolean checkP10CertRequest( + final byte[] transactionID, + final String requesterDn, + final byte[] pkcs10CertRequest, + final String requestedSubjectDn, + byte[] pkiMessage) { + fail(String.format( + "checkP10CertRequest called with transactionID: {}, requesterDn: {}, requestedSubjectDn: {}", + new BigInteger(transactionID), + requesterDn, + requestedSubjectDn)); + return false; + } + + @Override + public boolean learnEnrollmentResult( + final byte[] transactionID, + final byte[] certificate, + final String serialNumber, + final String subjectDN, + final String issuerDN) { + LOGGER.debug( + "learnEnrollmentResult called with transactionID: {}, serialNumber: {}, subjectDN: {}, issuerDN: {}", + new BigInteger(transactionID), + serialNumber, + subjectDN, + issuerDN); + return true; + } + }; + } + + @Override + public PersistencyInterface getPersistency() { + return persistency; + } + + @Override + public int getRetryAfterTimeInSeconds(final String certProfile, final int bodyType) { + LOGGER.debug( + "getRetryAfterTimeInSeconds called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return 1; + } + + @Override + public SupportMessageHandlerInterface getSupportMessageHandler( + final String certProfile, final String infoTypeOid) { + LOGGER.debug( + "getSupportMessageHandler called with certprofile: {}, infoTypeOid: {}", + certProfile, + infoTypeOid); + if (AttestationObjectIdentifiers.id_it_NonceRequest.getId().equalsIgnoreCase(infoTypeOid)) { + return new GetFreshRatNonceHandler() {}; + } + return null; + } + + @Override + public CmpMessageInterface getUpstreamConfiguration(final String certProfile, final int bodyType) { + LOGGER.debug( + "getUpstreamConfiguration called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return new CmpMessageInterface() { + + @Override + public VerificationContext getInputVerification() { + return upstreamTrust; + } + + @Override + public NestedEndpointContext getNestedEndpointContext() { + return null; + } + + @Override + public CredentialContext getOutputCredentials() { + + try { + return upstreamCredentials; + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public ReprotectMode getReprotectMode() { + return ReprotectMode.reprotect; + } + + @Override + public boolean getSuppressRedundantExtraCerts() { + return false; + } + + @Override + public boolean isCacheExtraCerts() { + return false; + } + + @Override + public boolean isMessageTimeDeviationAllowed(final long deviation) { + return true; + } + }; + } + + @Override + public boolean isRaVerifiedAcceptable(final String certProfile, final int bodyType) { + LOGGER.debug( + "isRaVerifiedAcceptable called with certprofile: {}, type: {}", + certProfile, + MessageDumper.msgTypeAsString(bodyType)); + return false; + } + }; + } + + ClientContext getRatClientContext(final int enrollmentType, KeyPair keyPair, boolean requestImplicitConfirm) { + return new ClientContext() { + + @Override + public ClientAttestationContext getAttestationContext() { + return new ClientAttestationContext() { + + @Override + public String getNonceRequestHint() { + return "nonce hint"; + } + + @Override + public String getNonceRequestType() { + return "1.9.8.7.6"; + } + + @Override + public BigInteger getNonceRequestLen() { + return new BigInteger("16"); + } + + @Override + public byte[] getNonceRequestVendorextension() { + return "vendor extension".getBytes(); + } + + @Override + public Certificate[] getEvidenceBundleCerts() { + // just provide some certificates + final SignatureValidationCredentials enrollmentTrust = + new SignatureValidationCredentials("credentials/ENROLL_Root.pem", null); + final Collection trustedCertificates = + enrollmentTrust.getTrustedCertificates(); + Certificate[] ret = new Certificate[trustedCertificates.size()]; + int i = 0; + for (X509Certificate akt : trustedCertificates) { + try { + ret[i++] = Certificate.getInstance(akt.getEncoded()); + } catch (CertificateEncodingException e) { + fail(e.getLocalizedMessage()); + } + } + return ret; + } + + @Override + public byte[] getEvidenceStatement(byte[] attestationNonce) { + + NonceResponse nonceResponse = NonceResponse.getInstance(attestationNonce); + assertNotNull(nonceResponse.getExpiry()); + assertNotNull(nonceResponse.getHint()); + assertNotNull(nonceResponse.getVendorextension()); + + try { + return new EvidenceStatement( + nonceResponse.getType().branch("88"), + new DERUTF8String("evidence statement"), + new DERIA5String("evidence hint")) + .getEncoded(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + @Override + public EnrollmentContext getEnrollmentContext() { + return new EnrollmentContext() { + + @Override + public KeyPair getCertificateKeypair() { + return keyPair; + } + + @Override + public VerificationContext getEnrollmentTrust() { + return enrollmentCredentials; + } + + @Override + public int getEnrollmentType() { + return enrollmentType; + } + + @Override + public List getExtensions() { + return null; + } + + @Override + public X509Certificate getOldCert() { + return null; + } + + @Override + public boolean getRequestImplictConfirm() { + return requestImplicitConfirm; + } + + @Override + public String getSubject() { + return "CN=Subject"; + } + }; + } + + @Override + public RevocationContext getRevocationContext() { + fail("getRevocationContext"); + return null; + } + }; + } + + @Before + public void setUp() throws Exception { + launchCmpCaAndRa(buildSignatureBasedDownstreamConfiguration()); + } + + @Test + public void testCr() throws Exception { + final EnrollmentResult ret = getSignatureBasedCmpClient( + "theCertProfileForOnlineEnrollment", + getRatClientContext( + PKIBody.TYPE_CERT_REQ, + ConfigurationFactory.getKeyGenerator().generateKeyPair(), + false), + UPSTREAM_TRUST_PATH) + .invokeEnrollment(); + final ASN1OctetString extValue = ASN1OctetString.getInstance( + ret.getEnrolledCertificate().getExtensionValue(AttestationObjectIdentifiers.id_aa_ar.getId())); + final AttestationResultBundle attestationResultBundle = + AttestationResultBundle.getInstance(extValue.getOctets()); + assertNotNull(attestationResultBundle); + assertNotNull(attestationResultBundle.getCerts()); + for (AttestationResult result : attestationResultBundle.getResults()) { + assertNotNull(result.getType()); + assertNotNull(result.getStmt()); + } + } + + @Test + public void testCrWithImplicitConfirm() throws Exception { + final EnrollmentResult ret = getSignatureBasedCmpClient( + "theCertProfileForOnlineEnrollment", + getRatClientContext( + PKIBody.TYPE_CERT_REQ, + ConfigurationFactory.getKeyGenerator().generateKeyPair(), + true), + UPSTREAM_TRUST_PATH) + .invokeEnrollment(); + final ASN1OctetString extValue = ASN1OctetString.getInstance( + ret.getEnrolledCertificate().getExtensionValue(AttestationObjectIdentifiers.id_aa_ar.getId())); + final AttestationResultBundle attestationResultBundle = + AttestationResultBundle.getInstance(extValue.getOctets()); + assertNotNull(attestationResultBundle); + assertNotNull(attestationResultBundle.getCerts()); + for (AttestationResult result : attestationResultBundle.getResults()) { + assertNotNull(result.getType()); + assertNotNull(result.getStmt()); + } + } +} diff --git a/src/test/java/com/siemens/pki/verifieradapter/asn1/TestRatAsn1.java b/src/test/java/com/siemens/pki/verifieradapter/asn1/TestRatAsn1.java new file mode 100644 index 00000000..c587b226 --- /dev/null +++ b/src/test/java/com/siemens/pki/verifieradapter/asn1/TestRatAsn1.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 Siemens AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.siemens.pki.verifieradapter.asn1; + +import static org.junit.Assert.*; + +import com.siemens.pki.verifieradapter.asn1.NonceRequestValue.NonceRequest; +import com.siemens.pki.verifieradapter.asn1.NonceResponseValue.NonceResponse; +import java.io.IOException; +import java.math.BigInteger; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERUTF8String; +import org.junit.Test; + +/** + * test some unusual parameter combinations to improve test coverage + */ +public class TestRatAsn1 { + + @Test + public void testAttestationResultBundle() throws IOException { + byte[] encoded = new AttestationResultBundle( + new AttestationResult[] { + new AttestationResult(new ASN1ObjectIdentifier("1.2.3"), new DERUTF8String("hallo")) + }, + null) + .getEncoded(); + AttestationResultBundle decoded = AttestationResultBundle.getInstance(encoded); + assertEquals(1, decoded.getResults().length); + final AttestationResult attestationResult = decoded.getResults()[0]; + assertEquals(new ASN1ObjectIdentifier("1.2.3"), attestationResult.getType()); + assertEquals(new DERUTF8String("hallo"), attestationResult.getStmt()); + assertNull(decoded.getCerts()); + } + + @Test + public void testEvidenceBundle() throws IOException { + byte[] encoded = new EvidenceBundle( + new EvidenceStatement[] { + new EvidenceStatement(new ASN1ObjectIdentifier("1.2.3"), new DERUTF8String("hallo"), null) + }, + null) + .getEncoded(); + EvidenceBundle decoded = EvidenceBundle.getInstance(encoded); + assertEquals(1, decoded.getEvidences().length); + final EvidenceStatement evidenceStatement = decoded.getEvidences()[0]; + assertEquals(new ASN1ObjectIdentifier("1.2.3"), evidenceStatement.getType()); + assertEquals(new DERUTF8String("hallo"), evidenceStatement.getStmt()); + assertNull(evidenceStatement.getHint()); + assertNull(decoded.getCerts()); + } + + @Test + public void testNonceRequestValue() throws IOException { + byte[] encoded = new NonceRequestValue( + new NonceRequest[] {new NonceRequest((BigInteger) null, null, null, null)}) + .getEncoded(); + NonceRequestValue decoded = NonceRequestValue.getInstance(encoded); + assertEquals(1, decoded.getNonceRequests().length); + final NonceRequest nonceRequest = decoded.getNonceRequests()[0]; + assertNull(nonceRequest.getLen()); + assertNull(nonceRequest.getType()); + assertNull(nonceRequest.getHint()); + assertNull(nonceRequest.getVendorextension()); + } + + @Test + public void testNonceResponseValue() throws IOException { + byte[] encoded = new NonceResponseValue( + new NonceResponse[] {new NonceResponse(new byte[10], null, null, null, null)}) + .getEncoded(); + NonceResponseValue decoded = NonceResponseValue.getInstance(encoded); + assertEquals(1, decoded.getNonceResponse().length); + final NonceResponse nonceRequest = decoded.getNonceResponse()[0]; + assertNotNull(nonceRequest.getNonce()); + assertNull(nonceRequest.getExpiry()); + assertNull(nonceRequest.getType()); + assertNull(nonceRequest.getHint()); + assertNull(nonceRequest.getVendorextension()); + } +}