Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Original file line number Diff line number Diff line change
@@ -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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, in https://datatracker.ietf.org/doc/draft-ietf-lamps-csr-attestation/ this meanwhile has been renamed to AttestationBundle.

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 <code>null</code>
*/
default String getNonceRequestHint() {
return null;
}

/**
* indicates the required length of the requested nonce
* @return length or <code>null</code>
*/
default BigInteger getNonceRequestLen() {
Comment thread
This conversation was marked as resolved.
return null;
}
/**
* indicates which Evidence type to request a nonce for
* @return OID formatted string or <code>null</code>
*/
default String getNonceRequestType() {
return null;
}

/**
* Siemens proprietary extension to carry additional data
* @return additional data or <code>null</code>
*/
default byte[] getNonceRequestVendorextension() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider getNonceRequestVendorExtension (camelCase) instead of getNonceRequestVendorextension.

return null;
}
/**
* get certs to include in {@link EvidenceBundle}
* @return certs
*/
default Certificate[] getEvidenceBundleCerts() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
*/
public interface ClientContext {

/**
* get remote attestation specific configuration
* @return remote attestation specific configuration
* or <code>null</code> is no remote attestation shall
* be used
*/
default ClientAttestationContext getAttestationContext() {
return null;
}

/**
* get enrollment specific configuration
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <code>null</code> if no certificate profile
* should be used.
*
Expand Down Expand Up @@ -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)
Comment on lines +187 to +191

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is simpler to keep at least part of these argument in some state fields and take them from there?
I suppose one could even avoid distinguishing between methods building initial vs. further requests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding (transaction) states global and non final makes it hard to implement concurrency and slows down the VM. So I would keep everything local and final as much as possible.

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);
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
* <code>null</code> if no certificate profile
* should be used.
*
Expand Down Expand Up @@ -409,6 +418,51 @@ private boolean grantsImplicitConfirm(final PKIMessage msg) {
public EnrollmentResult invokeEnrollment() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is around 200 lines of code with a high complexity.
I recommend refactoring it into smaller methods to improve readability and simplify unit testing. Helps future maintenance.


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});
Comment on lines +425 to +430

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here you generate a request for a single nonce.
Please add at least a TODO that this should be generalized to request for multiple nonces.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs change of
ClientAttestationContext ClientContext:getAttestationContext()
replace with
Collection <ClientAttestationContext> ClientContext:getAttestationContext()

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should throw an error if not getting exactly one ITAV

if (AttestationObjectIdentifiers.id_it_NonceResponse.equals(aktitav.getInfoType())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise should throw an error

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its about robustness -> Ignore, what you are unable to handle.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check that the number of nonces obtains equals the number requested, which so far is always 1.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness.

EvidenceStatement[] evidenceStatements = new EvidenceStatement[nonceResponses.length];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the number of evidence statements always follow the number of nonce responses?
Please add at least a respective TODO to generalize this.

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();

Expand Down Expand Up @@ -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<Extension> extensions = new ArrayList<>();

final List<TemplateExtension> 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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>null</code> if no certificate
* profile was specified
* @param bodyType request/response PKI Message Body type
* @return external RatVerifierAdapter or <code>null</code>
*/
default RatVerifierAdapter getVerifierAdapter(String certProfile, int bodyType) {
return null;
}

/**
* provide a persistence implementation
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public interface CrlUpdateRetrievalHandler extends SupportMessageHandlerInterfac
* <code>null</code> if absent in request
* @param issuer issuer from CRLSource or <code>null</code>
* if absent in request
* @param thisUpdate thisUpdate time from CRLStatus in request
* @param thisUpdate thisUpdate time from CRLStatus in request or
* <code>null</code> if absent in request
* @return CRLs to be returned or <code>null</code> if the returned infoValue
* should be absent
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Loading