se.idsec.signservice.integration.process.impl.DefaultSignerAssertionInfoProcessor Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2019-2023 IDsec Solutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package se.idsec.signservice.integration.process.impl;
import jakarta.annotation.Nonnull;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import se.idsec.signservice.integration.SignResponseProcessingParameters;
import se.idsec.signservice.integration.authentication.SignerAssertionInformation;
import se.idsec.signservice.integration.authentication.SignerAssertionInformation.SignerAssertionInformationBuilder;
import se.idsec.signservice.integration.authentication.SignerIdentityAttributeValue;
import se.idsec.signservice.integration.core.error.ErrorCode;
import se.idsec.signservice.integration.core.error.SignServiceIntegrationException;
import se.idsec.signservice.integration.core.error.impl.SignServiceProtocolException;
import se.idsec.signservice.integration.core.impl.CorrelationID;
import se.idsec.signservice.integration.dss.DssUtils;
import se.idsec.signservice.integration.dss.SignRequestWrapper;
import se.idsec.signservice.integration.dss.SignResponseWrapper;
import se.idsec.signservice.integration.process.SignResponseProcessingConfig;
import se.idsec.signservice.integration.signmessage.SignMessageProcessor;
import se.idsec.signservice.integration.state.SignatureSessionState;
import se.idsec.signservice.xml.DOMUtils;
import se.swedenconnect.schemas.csig.dssext_1_1.ContextInfo;
import se.swedenconnect.schemas.csig.dssext_1_1.MappedAttributeType;
import se.swedenconnect.schemas.csig.dssext_1_1.PreferredSAMLAttributeNameType;
import se.swedenconnect.schemas.csig.dssext_1_1.RequestedCertAttributes;
import se.swedenconnect.schemas.csig.dssext_1_1.SignerAssertionInfo;
import se.swedenconnect.schemas.saml_2_0.assertion.Assertion;
import se.swedenconnect.schemas.saml_2_0.assertion.AttributeStatement;
import se.swedenconnect.xml.jaxb.JAXBUnmarshaller;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Default implementation of the {@link SignerAssertionInfoProcessor} interface.
*
* @author Martin Lindström ([email protected])
* @author Stefan Santesson ([email protected])
*/
@Slf4j
public class DefaultSignerAssertionInfoProcessor implements SignerAssertionInfoProcessor {
/** Processing config. */
protected SignResponseProcessingConfig processingConfig =
SignResponseProcessingConfig.defaultSignResponseProcessingConfig();
/** {@inheritDoc} */
@Override
public SignerAssertionInformation processSignerAssertionInfo(
@Nonnull final SignResponseWrapper signResponse, @Nonnull final SignatureSessionState state,
final SignResponseProcessingParameters parameters) throws SignServiceIntegrationException {
final SignerAssertionInfo signerAssertionInfo = signResponse.getSignResponseExtension().getSignerAssertionInfo();
if (signerAssertionInfo == null) {
final String msg =
String.format("No SignerAssertionInfo available in SignResponse [request-id='%s']",
state.getSignRequest().getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
final SignerAssertionInformationBuilder builder = SignerAssertionInformation.builder();
final SignRequestWrapper signRequest = state.getSignRequest();
// Attributes
// Validate that we got all required attributes ...
//
final List attributes = this.processAttributes(signerAssertionInfo, signRequest);
builder.signerAttributes(attributes);
// Get ContextInfo values ...
//
final ContextInfo contextInfo = signerAssertionInfo.getContextInfo();
if (contextInfo == null) {
final String msg =
String.format("No SignerAssertionInfo/ContextInfo available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
// IdentityProvider
//
if (contextInfo.getIdentityProvider() == null
|| StringUtils.isBlank(contextInfo.getIdentityProvider().getValue())) {
final String msg = String.format(
"No SignerAssertionInfo/ContextInfo/IdentityProvider available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
if (!contextInfo.getIdentityProvider().getValue()
.equals(signRequest.getSignRequestExtension().getIdentityProvider().getValue())) {
final String msg =
String.format(
"IdentityProvider in SignResponse (%s) does not match provider given in SignRequest (%s) [request-id='%s']",
contextInfo.getIdentityProvider().getValue(),
signRequest.getSignRequestExtension().getIdentityProvider().getValue(),
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
builder.authnServiceID(contextInfo.getIdentityProvider().getValue());
// AssertionRef
//
final String assertionRef = contextInfo.getAssertionRef();
if (StringUtils.isBlank(assertionRef)) {
final String msg = String.format(
"No SignerAssertionInfo/ContextInfo/AssertionRef available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
builder.assertionReference(assertionRef);
// Assertions ...
//
byte[] idpAssertion = null;
if (!signerAssertionInfo.isSetSamlAssertions() || !signerAssertionInfo.getSamlAssertions().isSetAssertions()) {
if (this.processingConfig.isRequireAssertion()) {
final String msg =
String.format(
"No SignerAssertionInfo/SamlAssertions present in SignResponse. Configuration requires this [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
}
else {
if (signerAssertionInfo.getSamlAssertions().getAssertions().size() == 1
&& !this.processingConfig.isStrictProcessing()) {
// If strict processing is turned off, and we only got one assertion we trust that the SignService
// included the assertion that corresponds to AssertionRef.
//
idpAssertion = signerAssertionInfo.getSamlAssertions().getAssertions().get(0);
builder.assertion(Base64.getEncoder().encodeToString(idpAssertion));
}
// Find the assertion matching the AssertionRef ...
else {
for (final byte[] a : signerAssertionInfo.getSamlAssertions().getAssertions()) {
try {
final Assertion assertion = JAXBUnmarshaller.unmarshall(DOMUtils.bytesToDocument(a), Assertion.class);
if (assertionRef.equals(assertion.getID())) {
idpAssertion = a;
break;
}
else {
log.info("{}: Processing assertion with ID '%s' - no match with AssertionRef [request-id='{}']",
assertion.getID(), signRequest.getRequestID());
}
}
catch (final Exception e) {
final String msg = String.format(
"Invalid SAML assertion found in SignerAssertionInfo/SamlAssertions - %s [request-id='%s']",
e.getMessage(), signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
}
if (idpAssertion != null) {
builder.assertion(Base64.getEncoder().encodeToString(idpAssertion));
}
else {
final String msg =
String.format(
"No SAML assertion matching AssertionRef found in SignerAssertionInfo/SamlAssertions [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
}
}
// AuthenticationInstant
//
// This validation is essential. We want to ensure that the authentication instant is not too old. It must not be
// before we actually sent our request. Furthermore, it must not be after the sign response was sent. In both cases
// we take the allowed clock skew in account.
//
builder.authnInstant(
this.processAuthenticationInstant(contextInfo, signRequest, signResponse));
// AuthnContextClassRef
//
final String authnContextClassRef = contextInfo.getAuthnContextClassRef();
if (StringUtils.isBlank(authnContextClassRef)) {
final String msg = String.format(
"No SignerAssertionInfo/ContextInfo/AuthnContextClassRef available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
// Get hold of the LoA from the request.
final List requestedAuthnContextClassRefs =
Optional.ofNullable(signRequest.getSignRequestExtension().getCertRequestProperties().getAuthnContextClassRefs())
.orElse(Collections.emptyList());
// Did we require a sign message to be displayed?
final boolean requireDisplaySignMessageProof = this.requireDisplaySignMessageProof(state);
if (requestedAuthnContextClassRefs.contains(authnContextClassRef)) {
// If display of SignMessage was required, we need the signMessageDigest attribute to be released.
if (requireDisplaySignMessageProof) {
String signMessageDigest = attributes.stream()
.filter(a -> SignMessageProcessor.SIGN_MESSAGE_DIGEST_ATTRIBUTE.equals(a.getName()))
.map(SignerIdentityAttributeValue::getValue)
.findFirst()
.orElse(null);
// OK, the signMessageDigest wasn't part of the SignerAssertionInfo/AttributeStatement element.
// This is not really an error since version 1.3 of "DSS Extension for Federated Central Signing Services"
// states the following:
//
// [Required]
// This element of type saml:AttributeStatementType (see [SAML2.0]) holds subject attributes
// obtained from the SAML assertion used to authenticate the signer at the Signing Service.
// For integrity reasons, this element SHOULD only provide information about SAML attribute
// values that maps to subject identity information in the signer's certificate.
//
// So, lets hope that we have an assertion ...
//
if (idpAssertion != null) {
try {
final Assertion assertion =
JAXBUnmarshaller.unmarshall(DOMUtils.bytesToDocument(idpAssertion), Assertion.class);
final AttributeStatement attributeStatement = DssUtils.getAttributeStatement(assertion);
if (attributeStatement != null) {
signMessageDigest =
DssUtils.getAttributeValue(attributeStatement, SignMessageProcessor.SIGN_MESSAGE_DIGEST_ATTRIBUTE);
}
}
catch (final Exception e) {
final String msg =
String.format(
"Invalid SAML assertion found in SignerAssertionInfo/SamlAssertions - %s [request-id='%s']",
e.getMessage(), signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
}
if (signMessageDigest == null) {
final String msg = String.format(
"Missing proof for displayed sign message (no signMessageDigest and no sigmessage authnContext) [request-id='%s']",
signRequest.getRequestID());
if (this.processingConfig.isRequireAssertion()) {
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-authncontext"), msg);
}
else {
// If we did not require assertions to be delivered, we can't fail here. We have to trust
// that the sign service made sure that the signMessageDigest was received.
//
log.warn("{}: {}", CorrelationID.id(), msg);
}
}
else if (this.processingConfig.isStrictProcessing()) {
// Compare hash with our own hash of the sent SignMessage.
// TODO
}
}
}
else {
final String msg = String.format("Unexpected authnContextRef received - %s. %s was expected [request-id='%s']",
authnContextClassRef, requestedAuthnContextClassRefs, signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-authncontext"), msg);
}
builder.authnContextRef(authnContextClassRef);
// AuthType
//
builder.authnType(contextInfo.getAuthType());
return builder.build();
}
/**
* Tells if the display of a sign message with a MustShow flag set was requested.
*
* @param state the state
* @return if sign message display is required true is returned, otherwise false
*/
protected boolean requireDisplaySignMessageProof(final SignatureSessionState state) {
if (state.getSignMessage() != null && state.getSignMessage().getMustShow() != null) {
return state.getSignMessage().getMustShow();
}
return false;
}
/**
* Extracts the attributes from the response and validates that we received all attributes that were requested (if
* strict processing is enabled).
*
* @param signerAssertionInfo the signer info (including the received attributes)
* @param signRequest the request
* @return a list of attributes
* @throws SignServiceIntegrationException for validation errors
*/
protected List processAttributes(final SignerAssertionInfo signerAssertionInfo,
final SignRequestWrapper signRequest) throws SignServiceIntegrationException {
if (signerAssertionInfo.getAttributeStatement() == null
|| signerAssertionInfo.getAttributeStatement().getAttributesAndEncryptedAttributes().isEmpty()) {
final String msg =
String.format("No SignerAssertionInfo/AttributeStatement available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
final List attributes =
DssUtils.fromAttributeStatement(signerAssertionInfo.getAttributeStatement());
if (this.processingConfig.isStrictProcessing()) {
final RequestedCertAttributes requestedAttributes =
signRequest.getSignRequestExtension().getCertRequestProperties().getRequestedCertAttributes();
for (final MappedAttributeType mat : requestedAttributes.getRequestedCertAttributes()) {
if (!mat.isRequired() || StringUtils.isNotBlank(mat.getDefaultValue())) {
// For non required attributes or those having a default value there is no requirement to
// get it from the IdP or AA.
continue;
}
boolean attrDelivered = false;
for (final PreferredSAMLAttributeNameType attr : mat.getSamlAttributeNames()) {
if (attributes.stream().anyMatch(a -> a.getName().equals(attr.getValue()))) {
log.trace("{}: Requested attribute '{}' was delivered by IdP/AA [request-id='{}']",
CorrelationID.id(), attr.getValue(), signRequest.getRequestID());
attrDelivered = true;
break;
}
}
if (!attrDelivered) {
final String msg = String.format(
"None of the requested attribute(s) %s were delivered in SignerAssertionInfo/AttributeStatement [request-id='%s']",
mat.getSamlAttributeNames().stream().map(PreferredSAMLAttributeNameType::getValue)
.collect(Collectors.toList()),
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
}
}
return attributes;
}
/**
* Validates that the received authentication instant is OK.
*
* @param contextInfo the context info holding the authn instant
* @param signRequest the sign request
* @param signResponse the sign response
* @throws SignServiceIntegrationException for validation errors
*/
protected long processAuthenticationInstant(final ContextInfo contextInfo, final SignRequestWrapper signRequest,
final SignResponseWrapper signResponse)
throws SignServiceIntegrationException {
if (contextInfo.getAuthenticationInstant() == null) {
final String msg =
String.format(
"No SignerAssertionInfo/ContextInfo/AuthenticationInstant available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
final long authnInstant = contextInfo.getAuthenticationInstant().toGregorianCalendar().getTimeInMillis();
final long requestTime =
signRequest.getSignRequestExtension().getRequestTime().toGregorianCalendar().getTimeInMillis();
final long responseTime =
signResponse.getSignResponseExtension().getResponseTime().toGregorianCalendar().getTimeInMillis();
if (authnInstant + this.processingConfig.getAllowedClockSkew() < requestTime) {
final String msg = String.format(
"Invalid authentication instant (%d). It is before the SignRequest was sent (%d) [request-id='%s']",
authnInstant, requestTime, signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
if (authnInstant - this.processingConfig.getAllowedClockSkew() > responseTime) {
final String msg =
String.format("Invalid authentication instant (%d). It is after the SignResponse time (%d) [request-id='%s']",
authnInstant, responseTime, signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignResponseProcessingException(new ErrorCode.Code("invalid-response"), msg);
}
return authnInstant;
}
protected String processAuthnContextClassRef(final ContextInfo contextInfo, final SignRequestWrapper signRequest)
throws SignServiceIntegrationException {
final String authnContextClassRef = contextInfo.getAuthnContextClassRef();
if (StringUtils.isBlank(authnContextClassRef)) {
final String msg = String.format(
"No SignerAssertionInfo/ContextInfo/AuthnContextClassRef available in SignResponse [request-id='%s']",
signRequest.getRequestID());
log.error("{}: {}", CorrelationID.id(), msg);
throw new SignServiceProtocolException(msg);
}
// Get hold of the LoA from the request.
//
final List requestedAuthnContextClassRefs =
signRequest.getSignRequestExtension().getCertRequestProperties().getAuthnContextClassRefs();
if (requestedAuthnContextClassRefs != null && requestedAuthnContextClassRefs.contains(authnContextClassRef)) {
// OK if:
// If SM was required: signMessageDigest is present
// If not required - OK always
// AdditionalCheck: check hash
}
else {
// OK if:
// sigMessageUris allowed
// Mapping exists
// SignMessage was sent
}
return authnContextClassRef;
}
/**
* Assigns the processing config settings.
*
* @param processingConfig the processing config settings
*/
public void setProcessingConfig(final SignResponseProcessingConfig processingConfig) {
this.processingConfig = processingConfig;
}
/**
* Ensures that the {@code processingConfig} property is assigned. By default
* {@link SignResponseProcessingConfig#defaultSignResponseProcessingConfig()} is used.
*
*
* Note: If executing in a Spring Framework environment this method is automatically invoked after all properties have
* been assigned. Otherwise it should be explicitly invoked.
*
*/
@PostConstruct
public void afterPropertiesSet() {
if (this.processingConfig == null) {
this.processingConfig = SignResponseProcessingConfig.defaultSignResponseProcessingConfig();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy