Java tutorial
/** * Copyright (C) Posten Norge AS * * 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 no.digipost.api.interceptors; import no.digipost.api.security.OrgnummerExtractor; import no.digipost.api.xml.Constants; import org.apache.wss4j.common.ConfigurationConstants; import org.apache.wss4j.common.crypto.AlgorithmSuite; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.dom.WSConstants; import org.apache.wss4j.dom.WSDataRef; import org.apache.wss4j.dom.WSSConfig; import org.apache.wss4j.dom.WSSecurityEngine; import org.apache.wss4j.dom.WSSecurityEngineResult; import org.apache.wss4j.dom.handler.RequestData; import org.apache.wss4j.dom.handler.WSHandlerConstants; import org.apache.wss4j.dom.handler.WSHandlerResult; import org.apache.wss4j.dom.message.token.Timestamp; import org.apache.wss4j.dom.util.WSSecurityUtil; import org.apache.wss4j.dom.validate.Credential; import org.apache.wss4j.dom.validate.SignatureTrustValidator; import org.apache.wss4j.dom.validate.TimestampValidator; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.ws.context.MessageContext; import org.springframework.ws.soap.SoapMessage; import org.springframework.ws.soap.security.AbstractWsSecurityInterceptor; import org.springframework.ws.soap.security.WsSecurityFaultException; import org.springframework.ws.soap.security.WsSecuritySecurementException; import org.springframework.ws.soap.security.WsSecurityValidationException; import org.springframework.ws.soap.security.callback.CleanupCallback; import org.springframework.ws.soap.security.wss4j.Wss4jSecuritySecurementException; import org.springframework.ws.soap.security.wss4j.Wss4jSecurityValidationException; import org.w3c.dom.Document; import org.w3c.dom.Node; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.DigestMethod; import javax.xml.namespace.QName; import java.io.IOException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import static no.digipost.api.xml.Constants.HEADER_QNAME; import static no.digipost.api.xml.Constants.MESSAGING_QNAME; import static no.digipost.api.xml.Constants.WSSEC_HEADER_QNAME; import static no.digipost.api.xml.Constants.WSU_TIMESTAMP_QNAME; public class Wss4jInterceptor extends AbstractWsSecurityInterceptor { public static final String SECUREMENT_USER_PROPERTY_NAME = "Wss4jSecurityInterceptor.securementUser"; public static final String INCOMING_CERTIFICATE = "Wss4jInterceptor.incoming.certificate"; private String securementActions; private List<Integer> securementActionsVector; private String securementUsername; private CallbackHandler validationCallbackHandler; private String validationActions; private List<Integer> validationActionsVector; private String validationActor; private Crypto validationSignatureCrypto; private final boolean timestampStrict = true; private boolean enableSignatureConfirmation; private int validationTimeToLive = 120; private int securementTimeToLive = 120; private WSSConfig wssConfig; private final WSSecurityEngine securityEngine = new WSSecurityEngine(); private boolean enableRevocation; private final Wss4jHandler handler = new Wss4jHandler(); private String digestAlgorithm; private String securementSignatureAlgorithm; public Wss4jInterceptor() { setSecurementSignatureAlgorithm(Constants.RSA_SHA256); setSecurementSignatureDigestAlgorithm(DigestMethod.SHA256); setSecurementSignatureKeyIdentifier("DirectReference"); setSecurementActions("Timestamp Signature"); setValidationActions("Timestamp Signature"); } public void setSecurementActions(final String actions) { securementActions = actions; try { securementActionsVector = WSSecurityUtil.decodeAction(securementActions); } catch (WSSecurityException ex) { throw new IllegalArgumentException(ex); } } public void setSecurementActor(final String securementActor) { handler.setOption(WSHandlerConstants.ACTOR, securementActor); } public void setSecurementPassword(final String securementPassword) { handler.setSecurementPassword(securementPassword); } public void setSecurementSignatureAlgorithm(final String securementSignatureAlgorithm) { handler.setOption(WSHandlerConstants.SIG_ALGO, securementSignatureAlgorithm); this.securementSignatureAlgorithm = securementSignatureAlgorithm; } public void setSecurementSignatureDigestAlgorithm(final String digestAlgorithm) { handler.setOption(WSHandlerConstants.SIG_DIGEST_ALGO, digestAlgorithm); this.digestAlgorithm = digestAlgorithm; } public void setSecurementSignatureCrypto(final Crypto securementSignatureCrypto) { handler.setSecurementSignatureCrypto(securementSignatureCrypto); } public void setSecurementSignatureKeyIdentifier(final String securementSignatureKeyIdentifier) { handler.setOption(WSHandlerConstants.SIG_KEY_ID, securementSignatureKeyIdentifier); } public void setSecurementSignatureParts(final String securementSignatureParts) { handler.setOption(WSHandlerConstants.SIGNATURE_PARTS, securementSignatureParts); } public void setSecurementSignatureIfPresentParts(final String securementSignatureParts) { handler.setOption(ConfigurationConstants.OPTIONAL_SIGNATURE_PARTS, securementSignatureParts); } public void setSecurementSignatureUser(final String securementSignatureUser) { handler.setOption(WSHandlerConstants.SIGNATURE_USER, securementSignatureUser); } /** * Sets the time to live on the outgoing message */ public void setSecurementTimeToLive(final int ttl) { if (ttl <= 0) { throw new IllegalArgumentException("timeToLive must be positive"); } securementTimeToLive = ttl; } public void setValidationTimeToLive(final int ttl) { if (ttl <= 0) { throw new IllegalArgumentException("timeToLive must be positive"); } validationTimeToLive = ttl; } public void setValidationActions(final String actions) { validationActions = actions; try { validationActionsVector = WSSecurityUtil.decodeAction(actions); } catch (WSSecurityException ex) { throw new IllegalArgumentException(ex); } } public void setValidationSignatureCrypto(final Crypto signatureCrypto) { validationSignatureCrypto = signatureCrypto; } public void setEnableRevocation(final boolean enabled) { enableRevocation = enabled; } public void setBspCompliant(final boolean compliant) { handler.setOption(WSHandlerConstants.IS_BSP_COMPLIANT, compliant); } public void afterPropertiesSet() throws Exception { Assert.isTrue(validationActions != null || securementActions != null, "validationActions or securementActions are required"); if (validationActions != null) { if (validationActionsVector.contains(WSConstants.UT)) { Assert.notNull(validationCallbackHandler, "validationCallbackHandler is required"); } if (validationActionsVector.contains(WSConstants.SIGN)) { Assert.notNull(validationSignatureCrypto, "validationSignatureCrypto is required"); } } } @Override protected boolean handleFaultException(final WsSecurityFaultException ex, final MessageContext messageContext) { if (logger.isWarnEnabled()) { logger.warn("Could not handle request: " + ex.getMessage()); } throw new RuntimeException("Could not handle request", ex); } @Override protected void secureMessage(final SoapMessage soapMessage, final MessageContext messageContext) throws WsSecuritySecurementException { boolean noSecurity = securementActionsVector.isEmpty() || securementActionsVector.contains(0); if (noSecurity && !enableSignatureConfirmation) { return; } if (logger.isDebugEnabled()) { logger.debug("Securing message [" + soapMessage + "] with actions [" + securementActions + "]"); } RequestData requestData = initializeRequestData(messageContext); requestData.setAttachmentCallbackHandler(new AttachmentCallbackHandler(soapMessage)); Document envelopeAsDocument = soapMessage.getDocument(); try { // In case on signature confirmation with no other securement // action, we need to pass an empty securementActionsVector to avoid // NPE if (noSecurity) { securementActionsVector = new ArrayList<Integer>(0); } handler.doSenderAction(envelopeAsDocument, requestData, WSSecurityUtil.decodeHandlerAction(securementActions, wssConfig), true); } catch (WSSecurityException ex) { throw new Wss4jSecuritySecurementException(ex.getMessage(), ex); } soapMessage.setDocument(envelopeAsDocument); } /** * Creates and initializes a request data for the given message context. * * @param messageContext the message context * @return the request data */ protected RequestData initializeRequestData(final MessageContext messageContext) { RequestData requestData = new RequestData(); requestData.setMsgContext(messageContext); // reads securementUsername first from the context then from the property String contextUsername = (String) messageContext.getProperty(SECUREMENT_USER_PROPERTY_NAME); if (StringUtils.hasLength(contextUsername)) { requestData.setUsername(contextUsername); } else { requestData.setUsername(securementUsername); } requestData.setAppendSignatureAfterTimestamp(true); requestData.setTimeToLive(securementTimeToLive); requestData.setWssConfig(wssConfig); return requestData; } @Override protected void validateMessage(final SoapMessage soapMessage, final MessageContext messageContext) throws WsSecurityValidationException { if (logger.isDebugEnabled()) { logger.debug("Validating message [" + soapMessage + "] with actions [" + validationActions + "]"); } if (validationActionsVector.isEmpty() || validationActionsVector.contains(WSConstants.NO_SECURITY)) { return; } Document envelopeAsDocument = soapMessage.getDocument(); // Header processing try { RequestData requestData = new RequestData(); requestData.setAttachmentCallbackHandler(new AttachmentCallbackHandler(soapMessage)); requestData.setWssConfig(wssConfig); requestData.setSigVerCrypto(validationSignatureCrypto); requestData.setCallbackHandler(validationCallbackHandler); requestData.setSubjectCertConstraints(OrgnummerExtractor.PATTERNS); AlgorithmSuite algorithmSuite = new AlgorithmSuite(); algorithmSuite.addDigestAlgorithm(digestAlgorithm); algorithmSuite.addSignatureMethod(securementSignatureAlgorithm); algorithmSuite.addC14nAlgorithm(CanonicalizationMethod.EXCLUSIVE); //algorithmSuite.addTransformAlgorithm("http://www.w3.org/2001/10/xml-exc-c14n#"); //algorithmSuite.addTransformAlgorithm("http://docs.oasis-open.org/wss/2004/XX/oasis-2004XX-wss-swa-profile-1.0#Attachment-Complete-Transform"); requestData.setAlgorithmSuite(algorithmSuite); requestData.setEnableTimestampReplayCache(false); List<WSSecurityEngineResult> results = securityEngine.processSecurityHeader(envelopeAsDocument, validationActor, requestData); // Results verification if (CollectionUtils.isEmpty(results)) { throw new Wss4jSecurityValidationException("No WS-Security header found"); } updateMessageContextWithCertificate(messageContext, results); checkResults(results, validationActionsVector); validateEbmsMessagingIsSigned(envelopeAsDocument, results); validateTimestampIsSigned(envelopeAsDocument, results); // puts the results in the context // useful for Signature Confirmation updateContextWithResults(messageContext, results); verifyCertificateTrust(results); verifyTimestamp(results); } catch (WSSecurityException ex) { throw new Wss4jSecurityValidationException(ex.getMessage(), ex); } soapMessage.setDocument(envelopeAsDocument); soapMessage.getEnvelope().getHeader().removeHeaderElement(WS_SECURITY_NAME); } private void updateMessageContextWithCertificate(final MessageContext messageContext, final List<WSSecurityEngineResult> results) { WSSecurityEngineResult signResult = WSSecurityUtil.fetchActionResult(results, WSConstants.SIGN); if (signResult != null) { X509Certificate cert = (X509Certificate) signResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATE); if (cert != null) { messageContext.setProperty(INCOMING_CERTIFICATE, cert); } } } private void validateTimestampIsSigned(final Document doc, final List<WSSecurityEngineResult> results) { validateIsSigned(doc, results, HEADER_QNAME, WSSEC_HEADER_QNAME, WSU_TIMESTAMP_QNAME); } private void validateEbmsMessagingIsSigned(final Document doc, final List<WSSecurityEngineResult> results) { validateIsSigned(doc, results, HEADER_QNAME, MESSAGING_QNAME); } private void validateIsSigned(final Document doc, final List<WSSecurityEngineResult> results, final QName... qnamePath) { if (!wasSigned(doc, results, qnamePath)) { QName qName = qnamePath[qnamePath.length - 1]; throw new Wss4jSecurityValidationException( qName.getPrefix() + ":" + qName.getLocalPart() + " was not signed"); } } private boolean wasSigned(final Document doc, final List<WSSecurityEngineResult> results, final QName... qnamePath) { String path = "/" + doc.getDocumentElement().getPrefix() + ":Envelope"; for (QName qn : qnamePath) { Node n = doc.getDocumentElement().getElementsByTagNameNS(qn.getNamespaceURI(), qn.getLocalPart()) .item(0); if (n == null) { return false; } path += "/" + n.getPrefix() + ":" + n.getLocalName(); } for (WSSecurityEngineResult r : results) { if (r.containsKey("data-ref-uris")) { List<WSDataRef> refs = (List<WSDataRef>) r.get("data-ref-uris"); for (WSDataRef ref : refs) { if (ref.getName().equals(qnamePath[qnamePath.length - 1])) { if (ref.getXpath().equals(path)) { return true; } } } } } return false; } /** * Checks whether the received headers match the configured validation actions. Subclasses could override this method * for custom verification behavior. * * @param results the results of the validation function * @param validationActions the decoded validation actions * @throws Wss4jSecurityValidationException if the results are deemed invalid */ protected void checkResults(final List<WSSecurityEngineResult> results, final List<Integer> validationActions) throws Wss4jSecurityValidationException { if (!handler.checkReceiverResultsAnyOrder(results, validationActions)) { throw new Wss4jSecurityValidationException("Security processing failed (actions mismatch)"); } } /** * Puts the results of WS-Security headers processing in the message context. Some actions like Signature * Confirmation require this. */ @SuppressWarnings("unchecked") private void updateContextWithResults(final MessageContext messageContext, final List<WSSecurityEngineResult> results) { List<WSHandlerResult> handlerResults; if ((handlerResults = (List<WSHandlerResult>) messageContext .getProperty(WSHandlerConstants.RECV_RESULTS)) == null) { handlerResults = new ArrayList<WSHandlerResult>(); messageContext.setProperty(WSHandlerConstants.RECV_RESULTS, handlerResults); } WSHandlerResult rResult = new WSHandlerResult(validationActor, results); handlerResults.add(0, rResult); messageContext.setProperty(WSHandlerConstants.RECV_RESULTS, handlerResults); } /** * Verifies the trust of a certificate. */ protected void verifyCertificateTrust(final List<WSSecurityEngineResult> results) throws WSSecurityException { WSSecurityEngineResult actionResult = WSSecurityUtil.fetchActionResult(results, WSConstants.SIGN); if (actionResult != null) { X509Certificate returnCert = (X509Certificate) actionResult .get(WSSecurityEngineResult.TAG_X509_CERTIFICATE); Credential credential = new Credential(); credential.setCertificates(new X509Certificate[] { returnCert }); RequestData requestData = new RequestData(); requestData.setSigVerCrypto(validationSignatureCrypto); requestData.setEnableRevocation(enableRevocation); requestData.setSubjectCertConstraints(OrgnummerExtractor.PATTERNS); SignatureTrustValidator validator = new SignatureTrustValidator(); validator.validate(credential, requestData); } } /** * Verifies the timestamp. */ protected void verifyTimestamp(final List<WSSecurityEngineResult> results) throws WSSecurityException { WSSecurityEngineResult actionResult = WSSecurityUtil.fetchActionResult(results, WSConstants.TS); if (actionResult != null) { Timestamp timestamp = (Timestamp) actionResult.get(WSSecurityEngineResult.TAG_TIMESTAMP); if (timestamp != null && timestampStrict) { Credential credential = new Credential(); credential.setTimestamp(timestamp); RequestData requestData = new RequestData(); WSSConfig config = WSSConfig.getNewInstance(); config.setTimeStampTTL(validationTimeToLive); config.setTimeStampStrict(timestampStrict); requestData.setWssConfig(config); TimestampValidator validator = new TimestampValidator(); validator.validate(credential, requestData); } } } @Override protected void cleanUp() { if (validationCallbackHandler != null) { try { CleanupCallback cleanupCallback = new CleanupCallback(); validationCallbackHandler.handle(new Callback[] { cleanupCallback }); } catch (IOException ex) { logger.warn("Cleanup callback resulted in IOException", ex); } catch (UnsupportedCallbackException ex) { // ignore } } } }