Skip to content
Merged
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
1 change: 0 additions & 1 deletion .nvd-suppressions.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">

</suppressions>
14 changes: 14 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>

<!-- Azure Key Vault -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-keys</artifactId>
<version>4.2.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.0.9</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
62 changes: 36 additions & 26 deletions core/src/main/java/com/onelogin/saml2/authn/SamlResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.util.Objects;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import com.onelogin.saml2.model.hsm.HSM;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.slf4j.Logger;
Expand Down Expand Up @@ -78,7 +80,7 @@ public class SamlResponse {

/**
* After validation, if it fails this property has the cause of the problem
*/
*/
private Exception validationException;

/**
Expand Down Expand Up @@ -156,7 +158,7 @@ public void loadXmlFromBase64(String responseStr) throws ParserConfigurationExce

NodeList encryptedAssertionNodes = samlResponseDocument.getElementsByTagNameNS(Constants.NS_SAML,"EncryptedAssertion");

if (encryptedAssertionNodes.getLength() != 0) {
if (encryptedAssertionNodes.getLength() != 0) {
decryptedDocument = Util.copyDocument(samlResponseDocument);
encrypted = true;
decryptedDocument = this.decryptAssertion(decryptedDocument);
Expand Down Expand Up @@ -566,20 +568,20 @@ public String getNameIdSPNameQualifier() throws Exception {
* @throws XPathExpressionException
* @throws ValidationError
*
*/
*/
public HashMap<String, List<String>> getAttributes() throws XPathExpressionException, ValidationError {
HashMap<String, List<String>> attributes = new HashMap<String, List<String>>();

NodeList nodes = this.queryAssertion("/saml:AttributeStatement/saml:Attribute");

if (nodes.getLength() != 0) {
for (int i = 0; i < nodes.getLength(); i++) {
NamedNodeMap attrName = nodes.item(i).getAttributes();
String attName = attrName.getNamedItem("Name").getNodeValue();
if (attributes.containsKey(attName) && !settings.isAllowRepeatAttributeName()) {
throw new ValidationError("Found an Attribute element with duplicated Name", ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND);
}

NodeList childrens = nodes.item(i).getChildNodes();

List<String> attrValues = null;
Expand All @@ -605,7 +607,7 @@ public HashMap<String, List<String>> getAttributes() throws XPathExpressionExcep

/**
* Returns the ResponseStatus object
*
*
* @return
*/
public SamlResponseStatus getResponseStatus() {
Expand Down Expand Up @@ -639,7 +641,7 @@ public void checkStatus() throws ValidationError {
*
* @throws IllegalArgumentException
* if the response not contain status or if Unexpected XPath error
* @throws ValidationError
* @throws ValidationError
*/
public static SamlResponseStatus getStatus(Document dom) throws ValidationError {
String statusXpath = "/samlp:Response/samlp:Status";
Expand Down Expand Up @@ -682,7 +684,7 @@ public Boolean checkOneAuthnStatement() throws XPathExpressionException {
* Gets the audiences.
*
* @return the audiences of the response
*
*
* @throws XPathExpressionException
*/
public List<String> getAudiences() throws XPathExpressionException {
Expand All @@ -706,8 +708,8 @@ public List<String> getAudiences() throws XPathExpressionException {
*
* @return the issuers of the assertion/response
*
* @throws XPathExpressionException
* @throws ValidationError
* @throws XPathExpressionException
* @throws ValidationError
*/
public List<String> getIssuers() throws XPathExpressionException, ValidationError {
List<String> issuers = new ArrayList<String>();
Expand Down Expand Up @@ -763,7 +765,7 @@ public DateTime getSessionNotOnOrAfter() throws XPathExpressionException {
*
* @return the SessionIndex value
*
* @throws XPathExpressionException
* @throws XPathExpressionException
*/
public String getSessionIndex() throws XPathExpressionException {
String sessionIndex = null;
Expand Down Expand Up @@ -852,7 +854,7 @@ public ArrayList<String> processSignedElements() throws XPathExpressionException

String responseTag = "{" + Constants.NS_SAMLP + "}Response";
String assertionTag = "{" + Constants.NS_SAML + "}Assertion";

if (!signedElement.equals(responseTag) && !signedElement.equals(assertionTag)) {
throw new ValidationError("Invalid Signature Element " + signedElement + " SAML Response rejected", ValidationError.WRONG_SIGNED_ELEMENT);
}
Expand All @@ -862,13 +864,13 @@ public ArrayList<String> processSignedElements() throws XPathExpressionException
if (idNode == null || idNode.getNodeValue() == null || idNode.getNodeValue().isEmpty()) {
throw new ValidationError("Signed Element must contain an ID. SAML Response rejected", ValidationError.ID_NOT_FOUND_IN_SIGNED_ELEMENT);
}
String idValue = idNode.getNodeValue();

String idValue = idNode.getNodeValue();
if (verifiedIds.contains(idValue)) {
throw new ValidationError("Duplicated ID. SAML Response rejected", ValidationError.DUPLICATED_ID_IN_SIGNED_ELEMENTS);
}
verifiedIds.add(idValue);

NodeList refNodes = Util.query(null, "ds:SignedInfo/ds:Reference", signNode);
if (refNodes.getLength() == 1) {
Node refNode = refNodes.item(0);
Expand All @@ -878,7 +880,7 @@ public ArrayList<String> processSignedElements() throws XPathExpressionException
if (!sei.equals(idValue)) {
throw new ValidationError("Found an invalid Signed Element. SAML Response rejected", ValidationError.INVALID_SIGNED_ELEMENT);
}

if (verifiedSeis.contains(sei)) {
throw new ValidationError("Duplicated Reference URI. SAML Response rejected", ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS);
}
Expand Down Expand Up @@ -958,7 +960,7 @@ public boolean validateSignedElements(ArrayList<String> signedElements) throws X
*
* @return true if still valid
*
* @throws ValidationError
* @throws ValidationError
*/
public boolean validateTimestamps() throws ValidationError {
NodeList timestampNodes = samlResponseDocument.getElementsByTagNameNS("*", "Conditions");
Expand Down Expand Up @@ -1026,7 +1028,7 @@ public Exception getValidationException() {
* Xpath Expression
*
* @return the queried node
* @throws XPathExpressionException
* @throws XPathExpressionException
*
*/
private NodeList queryAssertion(String assertionXpath) throws XPathExpressionException {
Expand Down Expand Up @@ -1075,7 +1077,7 @@ private NodeList queryAssertion(String assertionXpath) throws XPathExpressionExc
*
* @param nameQuery
* Xpath Expression
* @param context
* @param context
* The context node
*
* @return DOMNodeList The queried nodes
Expand All @@ -1094,13 +1096,13 @@ private NodeList query(String nameQuery, Node context) throws XPathExpressionExc

/**
* Decrypt assertion.
*
*
* @param dom
* Encrypted assertion
*
* @return Decrypted Assertion.
*
* @throws XPathExpressionException
* @throws XPathExpressionException
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
Expand All @@ -1110,7 +1112,9 @@ private NodeList query(String nameQuery, Node context) throws XPathExpressionExc
private Document decryptAssertion(Document dom) throws XPathExpressionException, ParserConfigurationException, SAXException, IOException, SettingsException, ValidationError {
PrivateKey key = settings.getSPkey();

if (key == null) {
HSM hsm = this.settings.getHsm();

if (hsm == null && key == null) {
throw new SettingsException("No private key available for decrypt, check settings", SettingsException.PRIVATE_KEY_NOT_FOUND);
}

Expand All @@ -1119,7 +1123,13 @@ private Document decryptAssertion(Document dom) throws XPathExpressionException,
throw new ValidationError("No /samlp:Response/saml:EncryptedAssertion/xenc:EncryptedData element found", ValidationError.MISSING_ENCRYPTED_ELEMENT);
}
Element encryptedData = (Element) encryptedDataNodes.item(0);
Util.decryptElement(encryptedData, key);

if (hsm != null) {
Util.decryptUsingHsm(encryptedData, hsm);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you rename to decryptElementUsingHsm ?

} else {
Util.decryptElement(encryptedData, key);
}


// We need to Remove the saml:EncryptedAssertion Node
NodeList AssertionDataNodes = Util.query(dom, "/samlp:Response/saml:EncryptedAssertion/saml:Assertion");
Expand All @@ -1138,7 +1148,7 @@ private Document decryptAssertion(Document dom) throws XPathExpressionException,
}

/**
* @return the SAMLResponse XML, If the Assertion of the SAMLResponse was encrypted,
* @return the SAMLResponse XML, If the Assertion of the SAMLResponse was encrypted,
* returns the XML with the assertion decrypted
*/
public String getSAMLResponseXml() {
Expand All @@ -1148,11 +1158,11 @@ public String getSAMLResponseXml() {
} else {
xml = samlResponseString;
}
return xml;
return xml;
}

/**
* @return the SAMLResponse Document, If the Assertion of the SAMLResponse was encrypted,
* @return the SAMLResponse Document, If the Assertion of the SAMLResponse was encrypted,
* returns the Document with the assertion decrypted
*/
protected Document getSAMLResponseDocument() {
Expand Down
139 changes: 139 additions & 0 deletions core/src/main/java/com/onelogin/saml2/model/hsm/AzureKeyVault.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.onelogin.saml2.model.hsm;

import com.azure.core.http.HttpClient;
import com.azure.core.http.netty.NettyAsyncHttpClientBuilder;
import com.azure.identity.ClientSecretCredential;
import com.azure.identity.ClientSecretCredentialBuilder;
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
import com.azure.security.keyvault.keys.cryptography.models.EncryptionAlgorithm;
import com.azure.security.keyvault.keys.cryptography.models.KeyWrapAlgorithm;
import com.onelogin.saml2.util.Constants;

import java.util.HashMap;

public class AzureKeyVault extends HSM {

private String clientId;
private String clientCredentials;
private String tenantId;
private String keyVaultId;
private CryptographyClient akvClient;
private HashMap<String, KeyWrapAlgorithm> algorithmMapping;

/**
* Constructor to initialise an HSM object.
*
* @param clientId The Azure Key Vault client ID.
* @param clientCredentials The Azure Key Vault client credentials.
* @param tenantId The Azure Key Vault tenant ID.
* @param keyVaultId The Azure Key Vault ID.
* @return AzureKeyVault
*/
public AzureKeyVault(String clientId, String clientCredentials, String tenantId, String keyVaultId) {
this.clientId = clientId;
this.clientCredentials = clientCredentials;
this.tenantId = tenantId;
this.keyVaultId = keyVaultId;

this.algorithmMapping = createAlgorithmMapping();
}

/**
* Creates a mapping between the URLs received from the encrypted SAML
* assertion and the algorithms as how they are expected to be received from
* the Azure Key Vault.
*
* @return The algorithm mapping.
*/
private HashMap<String, KeyWrapAlgorithm> createAlgorithmMapping() {
HashMap<String, KeyWrapAlgorithm> mapping = new HashMap<>();

mapping.put(Constants.RSA_1_5, KeyWrapAlgorithm.RSA1_5);
mapping.put(Constants.RSA_OAEP_MGF1P, KeyWrapAlgorithm.RSA_OAEP);
mapping.put(Constants.A128KW, KeyWrapAlgorithm.A128KW);
mapping.put(Constants.A192KW, KeyWrapAlgorithm.A192KW);
mapping.put(Constants.A256KW, KeyWrapAlgorithm.A256KW);

return mapping;
}

/**
* Retrieves the key wrap algorithm object based on the algorithm URL passed
* within the SAML assertion.
*
* @param algorithmUrl The algorithm URL.
* @return The KeyWrapAlgorithm.
*/
private KeyWrapAlgorithm getAlgorithm(String algorithmUrl) {
return algorithmMapping.get(algorithmUrl);
}

/**
* Sets the client to connect to the Azure Key Vault.
*/
@Override
public void setClient() {
ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder()
.clientId(clientId)
.clientSecret(clientCredentials)
.tenantId(tenantId)
.build();

HttpClient httpClient = new NettyAsyncHttpClientBuilder().build();

this.akvClient = new CryptographyClientBuilder()
.httpClient(httpClient)
.credential(clientSecretCredential)
.keyIdentifier(keyVaultId)
.buildClient();
}

/**
* Wraps a key with a particular algorithm using the Azure Key Vault.
*
* @param algorithm The algorithm to use to wrap the key.
* @param key The key to wrap
* @return A wrapped key.
*/
@Override
public byte[] wrapKey(String algorithm, byte[] key) {
return this.akvClient.wrapKey(KeyWrapAlgorithm.fromString(algorithm), key).getEncryptedKey();
}

/**
* Unwraps a key with a particular algorithm using the Azure Key Vault.
*
* @param algorithmUrl The algorithm to use to unwrap the key.
* @param wrappedKey The key to unwrap
* @return An unwrapped key.
*/
@Override
public byte[] unwrapKey(String algorithmUrl, byte[] wrappedKey) {
return this.akvClient.unwrapKey(getAlgorithm(algorithmUrl), wrappedKey).getKey();
}

/**
* Encrypts an array of bytes with a particular algorithm using the Azure Key Vault.
*
* @param algorithm The algorithm to use for encryption.
* @param plainText The array of bytes to encrypt.
* @return An encrypted array of bytes.
*/
@Override
public byte[] encrypt(String algorithm, byte[] plainText) {
return this.akvClient.encrypt(EncryptionAlgorithm.fromString(algorithm), plainText).getCipherText();
}

/**
* Decrypts an array of bytes with a particular algorithm using the Azure Key Vault.
*
* @param algorithm The algorithm to use for decryption.
* @param cipherText The encrypted array of bytes.
* @return A decrypted array of bytes.
*/
@Override
public byte[] decrypt(String algorithm, byte[] cipherText) {
return this.akvClient.decrypt(EncryptionAlgorithm.fromString(algorithm), cipherText).getPlainText();
}
}
Loading