January 15th, 2015
WS-Trust Proof-of-Possession (PoP) tokens with client and server entropy (with partial keys) – Part 1
As a security feature WS-Trust
supports Proof-of-Possession
Tokens. In this post I want to show you how you can consume a service that requires PoP
token security with client and server entropy (going deep in a min). This method has been tested with Microsoft Dynamics CRM and ADFS.
This is a very long topic, so I will split it into two posts. This, the first one, will show how to request the token and calculate the PoP
key (shared key), in the second one I will show how to sign the request using this key.
But, lets start with a little of context…
Proof-of-Possession tokens (PoP) vs Bearer tokens
To start understanding a PoP
token we can compare it with its more common alternative: the Bearer
token.
Bearer
tokens are like cash, the mere fact of having one allows you to use it without any other step/verification. In the opposite way PoP
tokens are like credit card, the mere fact of possessing it is not enough, to use it you must prove that you are the real owner of it by using, for example, a valid id.
When we use Bearer
tokens the security of our applications rely on some transport encryption mechanism like TLS/SSL
, that is ok in some cases but there are some scenarios that require stronger security mechanisms. PoP
tokens add a security layer, because if someone captures one of your tokens he will still need to prove that he is the real owner of the token in order to use it on your application.
What is the problem with TLS/SSL?
Well, as I said, rely on TLS/SSL
is ok in some cases but in some others you want stronger security features. The first problem of relying only on TLS/SSL
is that you are putting all the eggs in one basket and in any 101 security class you will learn that the main security principle is layering your security.
On the other hand, TLS/SSL
security depends on the client application validating the certificate, other way you are completely exposed to Man-in-the-middle attacks. That means that you are moving the entire security of your service to the client and you know what? developers and TLS/SSL
don’t like each other.
If you google for “How to handle SSL certificate error” the first thing that you will find is a lot of ways of bypassing this validation in any platform. For example, Node.js
just changed the default from not validating to validating the certificate in version 0.10.x
.
Now, before of going on how WS-Trust
implements this feature, let me share with you the good new: there is some work on progress to take PoP
tokens to OAuth
. You can read more about that here: OAuth Proof-of-Possession Drafts.
WS-Trust and PoP Tokens
To start look at how we can use PoP
tokens on WS-Trust
lets look at the spec for a PoP
definition:
A proof-of-possession (POP) token is a security token that contains secret data that can be used to demonstrate authorized use of an associated security token, thereby the final service (relying party) can validate that the caller is the “real” owner of the token that he is presenting. Typically, although not exclusively, the POP token consist of a key known by the relying party.
Now, you need to know that WS-Trust
specifies two ways of use PoP
token keys: specific and partial.
When you use specific keys, the client can specify the key when he requests the token or the security token service (ADFS for example) can retrieve the key to be used. In both cases you just need to use the specific key to sign the requests you perform to the relying party (final service).
The partial keys scenario is a bit different because you will use two keys, one for the server, and one for the client, and the final key, the key with which you will sign the requests you perform to the relying party must be calculated combining both keys.
In this post I will explain the partial scenario that is the more complex, but at the same time, the more secure scenario.
Let’s start!
The first thing that you need to know if which type of keys does the service you want to call needs. You will find that described with WS-Policy
in the WSDL
file of the service:
<sp:Trust13 xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702"> <wsp:Policy> <sp:MustSupportIssuedTokens/> <sp:RequireClientEntropy/> <sp:RequireServerEntropy/> </wsp:Policy> </sp:Trust13>
In this example the service requires client and server entropy, that means partial keys.
Request the token
Now we need to request a token, I’ve explained how to request a token in this post from any platform and language, but it will be basically a SOAP
call to the WS-Trust
endpoint of our STS (for example ADFS). In the body of our SOAP
envelope we will need to specify the Request Security Token
(A predefined WS-Trust format to request tokens), the key we want to use:
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> <EndpointReference xmlns="http://www.w3.org/2005/08/addressing"> <Address>https://the-service-i-want-to-call.com/</Address> </EndpointReference> </wsp:AppliesTo> <t:Entropy> <t:BinarySecret Type="http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce"> yEEN5hsRamzDqFKmNqvp+3d2yzGOU+czcEeEXVJJ4fA= </t:BinarySecret> </t:Entropy> <t:KeySize>256</t:KeySize> <t:KeyType>http://schemas.xmlsoap.org/ws/2005/02/trust/SymmetricKey</t:KeyType> <t:ComputedKeyAlgorithm>http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1</t:ComputedKeyAlgorithm> </t:RequestSecurityToken>
This key yEEN5hsRamzDqFKmNqvp+3d2yzGOU+czcEeEXVJJ4fA=
, specified using the Entropy
tag will be our “Client Entropy”.
As you can see, we are also setting the KeySize
to 256, they KeytType
to symmetric key, and the ComputedKeyAlgorithm
to PSHA1, this is the algorithm that we are going to use to combine both keys. While it can be extended, the default algorithm is PSHA1
specified in the TLS Spec.
You have to have in mind that all of this parameters are configurable and depend on the Relying Party configuration, so you should read all of them from WSDL
file of the service you want to call, inside of the RequestSecurityTokenTemplate
element.
Now, after you send the request for the token, the server will reply something like this:
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> <t:Entropy> <t:BinarySecret>TUv/+WgHQYY2nR3kqB/5/Zac117tkBf2CkxWvs4G2pA=</t:BinarySecret> </t:Entropy> <t:RequestedSecurityToken> <xenc:EncryptedData>...</xenc:EncryptedData> </t:RequestedSecurityToken> </t:RequestSecurityTokenResponse>
I removed a lot of code to improve presentation, but inside of the response you will find two important elements, the first one is the Entropy
as you can imagine, this is the “Server entropy”, and the second one is the RequestedSecurityToken
containing the SAML Assertion. Now, in this scenario, the SAML Assertion must be encrypted to ensure that only the Relying Party (the service you want to call) can decrypt it because it will contain the key with which you need to sign the requests to prove that you are the real owner of this assertion.
Remember that the whole idea of this scenario is that if somehow your token is leaked the possession of it shouldn’t be enough to authenticate on the relying party (the service), so it doesn’t make sense to send the key with which you will prove that you are the real owner of the token inside of the assertion if it is not encrypted.
Calculate the shared secret (PoP key)
Now we have the three elements that we need to call the service: the SAML Assertion (encrypted) and the Client and Server Entropies.
The first thing that we need to do is calculate the final key with which we are going to sign the requests. To do that we are going to use the PSHA1
algorithm to combine both keys (client and server). As I said before, this algorithm is specified in the TLS Spec.
I’ve published a Node.js
module that implements the PSHA1
algorithm, you can find it here: https://www.npmjs.org/package/psha1. But for you to have an idea this is the Node.js
implementation:
var crypto = require('crypto'); module.exports = function (secret, seed, keySize) { keySize = keySize || 256; var clientBytes = new Buffer(secret, 'base64'); var serverBytes = new Buffer(seed, 'base64'); var sizeBytes = keySize / 8; var sha1DigestSizeBytes = 160 / 8; // 160 is the length of sha1 digest var buffer1 = serverBytes; var buffer2 = new Buffer(sha1DigestSizeBytes + serverBytes.length); var pshaBuffer = new Buffer(sizeBytes) var i = 0; var temp = null; while (i < sizeBytes) { buffer1 = new Buffer(crypto.createHmac('sha1', clientBytes) .update(buffer1).digest(), 'binary'); buffer1.copy(buffer2); serverBytes.copy(buffer2, sha1DigestSizeBytes); temp = new Buffer(crypto.createHmac('sha1', clientBytes) .update(buffer2).digest(), 'binary'); for (var x = 0; x < temp.length; x++) { if (i < sizeBytes) { pshaBuffer[i] = temp[x]; i++; } else { break; } }; } return pshaBuffer.toString('base64'); }
In this example, secret
is the client key and seed
is the server key. After applying this algorithm you will end up with the shared secret key (the final key that we are going to use to sign the request):
var sharedSecret = psha1(clientEntropy, serverEntropy, 256);
In the next post I will show how to sign the request to the service using this key… see you soon.