zaterdag 25 augustus 2018

How to manually generate a Signature for your SAML 2.0 Response


In this post we are going to look at two different ways to create a Signature over a SAML message. In a SAML based authentication flow the following messages are exchanged between the Service Provider (SP) and the Identity Provider (IdP):

  1. an AuthnRequest is sent from the SP to the backend of the IdP
  2. the IdP inspects the AuthnRequest and initiates a logon process for the user
  3. after successful authentication, a Response message is returned to the SP containing the SAML Assertion with information about the identity of the user
To ensure the integrity of the messages both the AuthnRequest and Response can be signed by the originator of the message, i.e. the SP signs the AuthnRequest and the IdP signs the Response. The signature of a signed SAML object can be verified by the receiver of the message by checking the signature against the public key of the originator.

For this post we going to zoom in on the signature over a Response object, which is really a wrapper object containing the SAML Assertion.

Using the Shibboleth Java OpenSAML library it is fairly easy to generate a Signature for a Response object. The code to do so could look like the following (using OpenSAML v3.2):

Signature signature = buildSAMLObject(Signature.class);
Credential credential = new BasicCredential(keyPair.getPublic(), keyPair.getPrivate());
signature.setSigningCredential(credential);
signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

response.setSignature(signature);

XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(response).marshall(response);
Signer.signObject(signature);

On the first line we create a org.opensaml.xmlsec.signature.Signature object using a helper function.

Next, we set up a Credential object using a pair of a private and public key. Here we are assuming that we have direct access to both the private and the public key, which is the case, for example, when both keys are stored in a keystore file which is accessible to this code fragment.

We then specify which algorithm we'd like to use for signing, in our case RSA-SHA256 and we specify the canonicalization format to be used to pre-process the XML to a standard format correctly handling whitespaces and comments.

The Signature object is marshalled into the Response object and the final line invokes the algorithm that creates the signature value and adds this value into the Response XML. This algorithm uses the private key passed in through the Credential object to create the signature. This is a clean and compact way to generate the Signature, building on the OpenSAML library which hides a lot of XML and PKI complexity for us.

However in some scenarios we may not have direct access to our private key and we can't use this approach. This happens, for example, when the private key isn't stored in a keystore but resides in a Hardware Security Module (HSM) or some other external hardware device and can only be accessed by an API. Through this API we have to specify the content we'd like to be signed using the private key and the response will contain the signature value. In this situation it's not possible to 'extract' the private key from the HSM and pass it into the OpenSAML Signature processing. Given that the OpenSAML does not support this kind of scenario we have to roll up our sleeves and perform the dirty work ourselves.

Let's start by inspecting in more detail what exactly happens when OpenSAML generates the Signature for a given Response (the protocol details are described here):
  1. canonicalize the Response XML 
  2. create a cryptographic hash over the canonicalized XML
  3. create a SignedInfo object containing references to the canonicalization algorithm and the hash value
  4. canonicalize the SignedInfo XML
  5. sign the canonicalized XML
  6. create a Signature XML section containing the SignedInfo as well as the signature value
  7. add the Signature to the original Response XML in the correct location
So if we can reproduce these steps without using the OpenSAML library we can insert our HSM-based signature process in step 5.

The following code snippets give some implementation detail about steps 1&2:

Document doc = getResponse();
Canonicalizer canonicalizer = Canonicalizer.getInstance(CanonicalizationMethod.EXCLUSIVE);
byte[] canonicalizedResponse = canonicalizer.canonicalizeSubtree(doc.getDocumentElement());
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashedCanonicalizedResponse = digest.digest(canonicalizedResponse);
String readableHash = baseEncode(hashedCanonicalizedResponse);

Nothing too exciting happening here - the result is a base64 encoded digest of the Response XML. This can be inserted into an SignedInfo XML section of the following form:

<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
  <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
  <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
  <ds:Reference URI="#1a3f38aac4327c6a8bfa6104ef220d38">
    <ds:Transforms>
      <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
      <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
    </ds:Transforms>
    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
    <ds:DigestValue>$readableHash</ds:DigestValue>
  </ds:Reference>
</ds:SignedInfo>

Now we can canonicalize the SignedInfo XML section and send it to the HSM API; here represented by a reference to a FakeHSM object:

Canonicalizer c14n = Canonicalizer.getInstance(CanonicalizationMethod.EXCLUSIVE);
byte[] canonicalizedSignedInfo = c14n.canonicalizeSubtree(signedInfoNode);
String signatureValue = fakeHsm.sign(canonicalizedSignedInfo);

Now that we have a signature value, the final steps are a matter of creating a Signature XML containing the SignedInfo section and the signature value. The resulting Signature can now be inserted into the original Response object, taking its XSD into account meaning that the Signature must follow the <Issuer> tag and precede the <Status> tag.

Summarizing, it is possible to create a Signature for your SAML Response or AuthnRequest objects even when you don't have direct access to your private key. However it is not a trivial task and this post has outlined the main steps in the process; should you be interested in all the details please feel free to download a working example from the accompanying GitHub repository https://github.com/willemvermeer/signatures/.

Geen opmerkingen: