Skip to content

Commit e756d62

Browse files
authored
Merge pull request #290 from web-frankenstein/master
Support for unwrapping key via an HSM when decrypting the SAML assertion.
2 parents 514224c + ca62eac commit e756d62

File tree

12 files changed

+433
-90
lines changed

12 files changed

+433
-90
lines changed

.nvd-suppressions.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
3-
43
</suppressions>

core/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@
6767
<artifactId>commons-codec</artifactId>
6868
<version>1.15</version>
6969
</dependency>
70+
71+
<!-- Azure Key Vault -->
72+
<dependency>
73+
<groupId>com.azure</groupId>
74+
<artifactId>azure-security-keyvault-keys</artifactId>
75+
<version>4.2.1</version>
76+
<optional>true</optional>
77+
</dependency>
78+
<dependency>
79+
<groupId>com.azure</groupId>
80+
<artifactId>azure-identity</artifactId>
81+
<version>1.0.9</version>
82+
<optional>true</optional>
83+
</dependency>
7084
</dependencies>
7185

7286
<build>

core/src/main/java/com/onelogin/saml2/authn/SamlResponse.java

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.Objects;
1111
import javax.xml.parsers.ParserConfigurationException;
1212
import javax.xml.xpath.XPathExpressionException;
13+
14+
import com.onelogin.saml2.model.hsm.HSM;
1315
import org.joda.time.DateTime;
1416
import org.joda.time.Instant;
1517
import org.slf4j.Logger;
@@ -78,7 +80,7 @@ public class SamlResponse {
7880

7981
/**
8082
* After validation, if it fails this property has the cause of the problem
81-
*/
83+
*/
8284
private Exception validationException;
8385

8486
/**
@@ -156,7 +158,7 @@ public void loadXmlFromBase64(String responseStr) throws ParserConfigurationExce
156158

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

159-
if (encryptedAssertionNodes.getLength() != 0) {
161+
if (encryptedAssertionNodes.getLength() != 0) {
160162
decryptedDocument = Util.copyDocument(samlResponseDocument);
161163
encrypted = true;
162164
decryptedDocument = this.decryptAssertion(decryptedDocument);
@@ -566,20 +568,20 @@ public String getNameIdSPNameQualifier() throws Exception {
566568
* @throws XPathExpressionException
567569
* @throws ValidationError
568570
*
569-
*/
571+
*/
570572
public HashMap<String, List<String>> getAttributes() throws XPathExpressionException, ValidationError {
571573
HashMap<String, List<String>> attributes = new HashMap<String, List<String>>();
572574

573575
NodeList nodes = this.queryAssertion("/saml:AttributeStatement/saml:Attribute");
574-
576+
575577
if (nodes.getLength() != 0) {
576578
for (int i = 0; i < nodes.getLength(); i++) {
577579
NamedNodeMap attrName = nodes.item(i).getAttributes();
578580
String attName = attrName.getNamedItem("Name").getNodeValue();
579581
if (attributes.containsKey(attName) && !settings.isAllowRepeatAttributeName()) {
580582
throw new ValidationError("Found an Attribute element with duplicated Name", ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND);
581583
}
582-
584+
583585
NodeList childrens = nodes.item(i).getChildNodes();
584586

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

606608
/**
607609
* Returns the ResponseStatus object
608-
*
610+
*
609611
* @return
610612
*/
611613
public SamlResponseStatus getResponseStatus() {
@@ -639,7 +641,7 @@ public void checkStatus() throws ValidationError {
639641
*
640642
* @throws IllegalArgumentException
641643
* if the response not contain status or if Unexpected XPath error
642-
* @throws ValidationError
644+
* @throws ValidationError
643645
*/
644646
public static SamlResponseStatus getStatus(Document dom) throws ValidationError {
645647
String statusXpath = "/samlp:Response/samlp:Status";
@@ -682,7 +684,7 @@ public Boolean checkOneAuthnStatement() throws XPathExpressionException {
682684
* Gets the audiences.
683685
*
684686
* @return the audiences of the response
685-
*
687+
*
686688
* @throws XPathExpressionException
687689
*/
688690
public List<String> getAudiences() throws XPathExpressionException {
@@ -706,8 +708,8 @@ public List<String> getAudiences() throws XPathExpressionException {
706708
*
707709
* @return the issuers of the assertion/response
708710
*
709-
* @throws XPathExpressionException
710-
* @throws ValidationError
711+
* @throws XPathExpressionException
712+
* @throws ValidationError
711713
*/
712714
public List<String> getIssuers() throws XPathExpressionException, ValidationError {
713715
List<String> issuers = new ArrayList<String>();
@@ -763,7 +765,7 @@ public DateTime getSessionNotOnOrAfter() throws XPathExpressionException {
763765
*
764766
* @return the SessionIndex value
765767
*
766-
* @throws XPathExpressionException
768+
* @throws XPathExpressionException
767769
*/
768770
public String getSessionIndex() throws XPathExpressionException {
769771
String sessionIndex = null;
@@ -852,7 +854,7 @@ public ArrayList<String> processSignedElements() throws XPathExpressionException
852854

853855
String responseTag = "{" + Constants.NS_SAMLP + "}Response";
854856
String assertionTag = "{" + Constants.NS_SAML + "}Assertion";
855-
857+
856858
if (!signedElement.equals(responseTag) && !signedElement.equals(assertionTag)) {
857859
throw new ValidationError("Invalid Signature Element " + signedElement + " SAML Response rejected", ValidationError.WRONG_SIGNED_ELEMENT);
858860
}
@@ -862,13 +864,13 @@ public ArrayList<String> processSignedElements() throws XPathExpressionException
862864
if (idNode == null || idNode.getNodeValue() == null || idNode.getNodeValue().isEmpty()) {
863865
throw new ValidationError("Signed Element must contain an ID. SAML Response rejected", ValidationError.ID_NOT_FOUND_IN_SIGNED_ELEMENT);
864866
}
865-
866-
String idValue = idNode.getNodeValue();
867+
868+
String idValue = idNode.getNodeValue();
867869
if (verifiedIds.contains(idValue)) {
868870
throw new ValidationError("Duplicated ID. SAML Response rejected", ValidationError.DUPLICATED_ID_IN_SIGNED_ELEMENTS);
869871
}
870872
verifiedIds.add(idValue);
871-
873+
872874
NodeList refNodes = Util.query(null, "ds:SignedInfo/ds:Reference", signNode);
873875
if (refNodes.getLength() == 1) {
874876
Node refNode = refNodes.item(0);
@@ -878,7 +880,7 @@ public ArrayList<String> processSignedElements() throws XPathExpressionException
878880
if (!sei.equals(idValue)) {
879881
throw new ValidationError("Found an invalid Signed Element. SAML Response rejected", ValidationError.INVALID_SIGNED_ELEMENT);
880882
}
881-
883+
882884
if (verifiedSeis.contains(sei)) {
883885
throw new ValidationError("Duplicated Reference URI. SAML Response rejected", ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS);
884886
}
@@ -958,7 +960,7 @@ public boolean validateSignedElements(ArrayList<String> signedElements) throws X
958960
*
959961
* @return true if still valid
960962
*
961-
* @throws ValidationError
963+
* @throws ValidationError
962964
*/
963965
public boolean validateTimestamps() throws ValidationError {
964966
NodeList timestampNodes = samlResponseDocument.getElementsByTagNameNS("*", "Conditions");
@@ -1026,7 +1028,7 @@ public Exception getValidationException() {
10261028
* Xpath Expression
10271029
*
10281030
* @return the queried node
1029-
* @throws XPathExpressionException
1031+
* @throws XPathExpressionException
10301032
*
10311033
*/
10321034
private NodeList queryAssertion(String assertionXpath) throws XPathExpressionException {
@@ -1075,7 +1077,7 @@ private NodeList queryAssertion(String assertionXpath) throws XPathExpressionExc
10751077
*
10761078
* @param nameQuery
10771079
* Xpath Expression
1078-
* @param context
1080+
* @param context
10791081
* The context node
10801082
*
10811083
* @return DOMNodeList The queried nodes
@@ -1094,13 +1096,13 @@ private NodeList query(String nameQuery, Node context) throws XPathExpressionExc
10941096

10951097
/**
10961098
* Decrypt assertion.
1097-
*
1099+
*
10981100
* @param dom
10991101
* Encrypted assertion
11001102
*
11011103
* @return Decrypted Assertion.
11021104
*
1103-
* @throws XPathExpressionException
1105+
* @throws XPathExpressionException
11041106
* @throws IOException
11051107
* @throws SAXException
11061108
* @throws ParserConfigurationException
@@ -1110,7 +1112,9 @@ private NodeList query(String nameQuery, Node context) throws XPathExpressionExc
11101112
private Document decryptAssertion(Document dom) throws XPathExpressionException, ParserConfigurationException, SAXException, IOException, SettingsException, ValidationError {
11111113
PrivateKey key = settings.getSPkey();
11121114

1113-
if (key == null) {
1115+
HSM hsm = this.settings.getHsm();
1116+
1117+
if (hsm == null && key == null) {
11141118
throw new SettingsException("No private key available for decrypt, check settings", SettingsException.PRIVATE_KEY_NOT_FOUND);
11151119
}
11161120

@@ -1119,7 +1123,13 @@ private Document decryptAssertion(Document dom) throws XPathExpressionException,
11191123
throw new ValidationError("No /samlp:Response/saml:EncryptedAssertion/xenc:EncryptedData element found", ValidationError.MISSING_ENCRYPTED_ELEMENT);
11201124
}
11211125
Element encryptedData = (Element) encryptedDataNodes.item(0);
1122-
Util.decryptElement(encryptedData, key);
1126+
1127+
if (hsm != null) {
1128+
Util.decryptUsingHsm(encryptedData, hsm);
1129+
} else {
1130+
Util.decryptElement(encryptedData, key);
1131+
}
1132+
11231133

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

11401150
/**
1141-
* @return the SAMLResponse XML, If the Assertion of the SAMLResponse was encrypted,
1151+
* @return the SAMLResponse XML, If the Assertion of the SAMLResponse was encrypted,
11421152
* returns the XML with the assertion decrypted
11431153
*/
11441154
public String getSAMLResponseXml() {
@@ -1148,11 +1158,11 @@ public String getSAMLResponseXml() {
11481158
} else {
11491159
xml = samlResponseString;
11501160
}
1151-
return xml;
1161+
return xml;
11521162
}
11531163

11541164
/**
1155-
* @return the SAMLResponse Document, If the Assertion of the SAMLResponse was encrypted,
1165+
* @return the SAMLResponse Document, If the Assertion of the SAMLResponse was encrypted,
11561166
* returns the Document with the assertion decrypted
11571167
*/
11581168
protected Document getSAMLResponseDocument() {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.onelogin.saml2.model.hsm;
2+
3+
import com.azure.core.http.HttpClient;
4+
import com.azure.core.http.netty.NettyAsyncHttpClientBuilder;
5+
import com.azure.identity.ClientSecretCredential;
6+
import com.azure.identity.ClientSecretCredentialBuilder;
7+
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
8+
import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
9+
import com.azure.security.keyvault.keys.cryptography.models.EncryptionAlgorithm;
10+
import com.azure.security.keyvault.keys.cryptography.models.KeyWrapAlgorithm;
11+
import com.onelogin.saml2.util.Constants;
12+
13+
import java.util.HashMap;
14+
15+
public class AzureKeyVault extends HSM {
16+
17+
private String clientId;
18+
private String clientCredentials;
19+
private String tenantId;
20+
private String keyVaultId;
21+
private CryptographyClient akvClient;
22+
private HashMap<String, KeyWrapAlgorithm> algorithmMapping;
23+
24+
/**
25+
* Constructor to initialise an HSM object.
26+
*
27+
* @param clientId The Azure Key Vault client ID.
28+
* @param clientCredentials The Azure Key Vault client credentials.
29+
* @param tenantId The Azure Key Vault tenant ID.
30+
* @param keyVaultId The Azure Key Vault ID.
31+
* @return AzureKeyVault
32+
*/
33+
public AzureKeyVault(String clientId, String clientCredentials, String tenantId, String keyVaultId) {
34+
this.clientId = clientId;
35+
this.clientCredentials = clientCredentials;
36+
this.tenantId = tenantId;
37+
this.keyVaultId = keyVaultId;
38+
39+
this.algorithmMapping = createAlgorithmMapping();
40+
}
41+
42+
/**
43+
* Creates a mapping between the URLs received from the encrypted SAML
44+
* assertion and the algorithms as how they are expected to be received from
45+
* the Azure Key Vault.
46+
*
47+
* @return The algorithm mapping.
48+
*/
49+
private HashMap<String, KeyWrapAlgorithm> createAlgorithmMapping() {
50+
HashMap<String, KeyWrapAlgorithm> mapping = new HashMap<>();
51+
52+
mapping.put(Constants.RSA_1_5, KeyWrapAlgorithm.RSA1_5);
53+
mapping.put(Constants.RSA_OAEP_MGF1P, KeyWrapAlgorithm.RSA_OAEP);
54+
mapping.put(Constants.A128KW, KeyWrapAlgorithm.A128KW);
55+
mapping.put(Constants.A192KW, KeyWrapAlgorithm.A192KW);
56+
mapping.put(Constants.A256KW, KeyWrapAlgorithm.A256KW);
57+
58+
return mapping;
59+
}
60+
61+
/**
62+
* Retrieves the key wrap algorithm object based on the algorithm URL passed
63+
* within the SAML assertion.
64+
*
65+
* @param algorithmUrl The algorithm URL.
66+
* @return The KeyWrapAlgorithm.
67+
*/
68+
private KeyWrapAlgorithm getAlgorithm(String algorithmUrl) {
69+
return algorithmMapping.get(algorithmUrl);
70+
}
71+
72+
/**
73+
* Sets the client to connect to the Azure Key Vault.
74+
*/
75+
@Override
76+
public void setClient() {
77+
ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder()
78+
.clientId(clientId)
79+
.clientSecret(clientCredentials)
80+
.tenantId(tenantId)
81+
.build();
82+
83+
HttpClient httpClient = new NettyAsyncHttpClientBuilder().build();
84+
85+
this.akvClient = new CryptographyClientBuilder()
86+
.httpClient(httpClient)
87+
.credential(clientSecretCredential)
88+
.keyIdentifier(keyVaultId)
89+
.buildClient();
90+
}
91+
92+
/**
93+
* Wraps a key with a particular algorithm using the Azure Key Vault.
94+
*
95+
* @param algorithm The algorithm to use to wrap the key.
96+
* @param key The key to wrap
97+
* @return A wrapped key.
98+
*/
99+
@Override
100+
public byte[] wrapKey(String algorithm, byte[] key) {
101+
return this.akvClient.wrapKey(KeyWrapAlgorithm.fromString(algorithm), key).getEncryptedKey();
102+
}
103+
104+
/**
105+
* Unwraps a key with a particular algorithm using the Azure Key Vault.
106+
*
107+
* @param algorithmUrl The algorithm to use to unwrap the key.
108+
* @param wrappedKey The key to unwrap
109+
* @return An unwrapped key.
110+
*/
111+
@Override
112+
public byte[] unwrapKey(String algorithmUrl, byte[] wrappedKey) {
113+
return this.akvClient.unwrapKey(getAlgorithm(algorithmUrl), wrappedKey).getKey();
114+
}
115+
116+
/**
117+
* Encrypts an array of bytes with a particular algorithm using the Azure Key Vault.
118+
*
119+
* @param algorithm The algorithm to use for encryption.
120+
* @param plainText The array of bytes to encrypt.
121+
* @return An encrypted array of bytes.
122+
*/
123+
@Override
124+
public byte[] encrypt(String algorithm, byte[] plainText) {
125+
return this.akvClient.encrypt(EncryptionAlgorithm.fromString(algorithm), plainText).getCipherText();
126+
}
127+
128+
/**
129+
* Decrypts an array of bytes with a particular algorithm using the Azure Key Vault.
130+
*
131+
* @param algorithm The algorithm to use for decryption.
132+
* @param cipherText The encrypted array of bytes.
133+
* @return A decrypted array of bytes.
134+
*/
135+
@Override
136+
public byte[] decrypt(String algorithm, byte[] cipherText) {
137+
return this.akvClient.decrypt(EncryptionAlgorithm.fromString(algorithm), cipherText).getPlainText();
138+
}
139+
}

0 commit comments

Comments
 (0)