/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.wss4j.dom.saml; import java.security.Principal; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.principal.WSDerivedKeyTokenPrincipal; import org.apache.wss4j.common.saml.OpenSAMLUtil; import org.apache.wss4j.common.saml.SAMLKeyInfo; import org.apache.wss4j.common.saml.SamlAssertionWrapper; import org.apache.wss4j.dom.WSConstants; import org.apache.wss4j.dom.WSDataRef; import org.apache.wss4j.dom.engine.WSSecurityEngineResult; import org.apache.wss4j.dom.handler.WSHandlerResult; import org.w3c.dom.Element; /** * Some SAML Utility methods only for use in the DOM code. */ public final class DOMSAMLUtil { private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(DOMSAMLUtil.class); private DOMSAMLUtil() { // complete } public static void validateSAMLResults( WSHandlerResult handlerResults, Certificate[] tlsCerts, Element body ) throws WSSecurityException { List<WSSecurityEngineResult> samlResults = new ArrayList<>(); if (handlerResults.getActionResults().containsKey(WSConstants.ST_SIGNED)) { samlResults.addAll(handlerResults.getActionResults().get(WSConstants.ST_SIGNED)); } if (handlerResults.getActionResults().containsKey(WSConstants.ST_UNSIGNED)) { samlResults.addAll(handlerResults.getActionResults().get(WSConstants.ST_UNSIGNED)); } if (samlResults.isEmpty()) { return; } List<WSSecurityEngineResult> signedResults = new ArrayList<>(); if (handlerResults.getActionResults().containsKey(WSConstants.SIGN)) { signedResults.addAll(handlerResults.getActionResults().get(WSConstants.SIGN)); } if (handlerResults.getActionResults().containsKey(WSConstants.UT_SIGN)) { signedResults.addAll(handlerResults.getActionResults().get(WSConstants.UT_SIGN)); } for (WSSecurityEngineResult samlResult : samlResults) { SamlAssertionWrapper assertionWrapper = (SamlAssertionWrapper)samlResult.get(WSSecurityEngineResult.TAG_SAML_ASSERTION); if (!checkHolderOfKey(assertionWrapper, signedResults, tlsCerts)) { LOG.warn("Assertion fails holder-of-key requirements"); throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY); } if (!checkSenderVouches(assertionWrapper, tlsCerts, body, signedResults)) { LOG.warn("Assertion fails sender-vouches requirements"); throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY); } } } /** * Check the holder-of-key requirements against the received assertion. The subject * credential of the SAML Assertion must have been used to sign some portion of * the message, thus showing proof-of-possession of the private/secret key. Alternatively, * the subject credential of the SAML Assertion must match a client certificate credential * when 2-way TLS is used. * @param assertionWrapper the SAML Assertion wrapper object * @param signedResults a list of all of the signed results */ public static boolean checkHolderOfKey( SamlAssertionWrapper assertionWrapper, List<WSSecurityEngineResult> signedResults, Certificate[] tlsCerts ) { List<String> confirmationMethods = assertionWrapper.getConfirmationMethods(); boolean isHolderOfKey = false; for (String confirmationMethod : confirmationMethods) { if (OpenSAMLUtil.isMethodHolderOfKey(confirmationMethod)) { isHolderOfKey = true; break; } } if (isHolderOfKey) { if (tlsCerts == null && (signedResults == null || signedResults.isEmpty())) { return false; } SAMLKeyInfo subjectKeyInfo = assertionWrapper.getSubjectKeyInfo(); if (!compareCredentials(subjectKeyInfo, signedResults, tlsCerts)) { return false; } } return true; } /** * Compare the credentials of the assertion to the credentials used in 2-way TLS or those * used to verify signatures. * Return true on a match * @param subjectKeyInfo the SAMLKeyInfo object * @param signedResults a list of all of the signed results * @return true if the credentials of the assertion were used to verify a signature */ public static boolean compareCredentials( SAMLKeyInfo subjectKeyInfo, List<WSSecurityEngineResult> signedResults, Certificate[] tlsCerts ) { X509Certificate[] subjectCerts = subjectKeyInfo.getCerts(); PublicKey subjectPublicKey = subjectKeyInfo.getPublicKey(); byte[] subjectSecretKey = subjectKeyInfo.getSecret(); // // Try to match the TLS certs first // if (tlsCerts != null && tlsCerts.length > 0 && subjectCerts != null && subjectCerts.length > 0 && tlsCerts[0].equals(subjectCerts[0])) { return true; } else if (tlsCerts != null && tlsCerts.length > 0 && subjectPublicKey != null && tlsCerts[0].getPublicKey().equals(subjectPublicKey)) { return true; } if (subjectPublicKey == null && subjectCerts != null && subjectCerts.length > 0) { subjectPublicKey = subjectCerts[0].getPublicKey(); } // // Now try the message-level signatures // for (WSSecurityEngineResult signedResult : signedResults) { X509Certificate[] certs = (X509Certificate[])signedResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATES); PublicKey publicKey = (PublicKey)signedResult.get(WSSecurityEngineResult.TAG_PUBLIC_KEY); byte[] secretKey = (byte[])signedResult.get(WSSecurityEngineResult.TAG_SECRET); if (certs != null && certs.length > 0 && subjectCerts != null && subjectCerts.length > 0 && certs[0].equals(subjectCerts[0])) { return true; } if (publicKey != null && publicKey.equals(subjectPublicKey)) { return true; } if (checkSecretKey(secretKey, subjectSecretKey, signedResult)) { return true; } } return false; } private static boolean checkSecretKey( byte[] secretKey, byte[] subjectSecretKey, WSSecurityEngineResult signedResult ) { if (secretKey != null && subjectSecretKey != null) { if (Arrays.equals(secretKey, subjectSecretKey)) { return true; } else { Principal principal = (Principal)signedResult.get(WSSecurityEngineResult.TAG_PRINCIPAL); if (principal instanceof WSDerivedKeyTokenPrincipal) { secretKey = ((WSDerivedKeyTokenPrincipal)principal).getSecret(); if (Arrays.equals(secretKey, subjectSecretKey)) { return true; } } } } return false; } /** * Check the sender-vouches requirements against the received assertion. The SAML * Assertion and the SOAP Body must be signed by the same signature. */ public static boolean checkSenderVouches( SamlAssertionWrapper assertionWrapper, Certificate[] tlsCerts, Element body, List<WSSecurityEngineResult> signed ) { // // If we have a 2-way TLS connection, then we don't have to check that the // assertion + SOAP body are signed // if (tlsCerts != null && tlsCerts.length > 0) { return true; } List<String> confirmationMethods = assertionWrapper.getConfirmationMethods(); boolean isSenderVouches = false; for (String confirmationMethod : confirmationMethods) { if (OpenSAMLUtil.isMethodSenderVouches(confirmationMethod)) { isSenderVouches = true; break; } } if (isSenderVouches) { if (signed == null || signed.isEmpty()) { return false; } if (!checkAssertionAndBodyAreSigned(assertionWrapper, body, signed)) { return false; } } return true; } /** * Return true if there is a signature which references the Assertion and the SOAP Body. * @param assertionWrapper the SamlAssertionWrapper object * @param body The SOAP body * @param signed The List of signed results * @return true if there is a signature which references the Assertion and the SOAP Body. */ private static boolean checkAssertionAndBodyAreSigned( SamlAssertionWrapper assertionWrapper, Element body, List<WSSecurityEngineResult> signed ) { for (WSSecurityEngineResult signedResult : signed) { @SuppressWarnings("unchecked") List<WSDataRef> sl = (List<WSDataRef>)signedResult.get( WSSecurityEngineResult.TAG_DATA_REF_URIS ); boolean assertionIsSigned = false; boolean bodyIsSigned = false; if (sl != null) { for (WSDataRef dataRef : sl) { Element se = dataRef.getProtectedElement(); if (se == assertionWrapper.getElement()) { assertionIsSigned = true; } if (se == body) { bodyIsSigned = true; } if (assertionIsSigned && bodyIsSigned) { return true; } } } } return false; } }