Friday, June 01, 2012

ECDH-ES for JSON Web Encryption

The JSON WebToken spec RECOMMENDS that ECDH-ES is implemented. Here we go:

Here are the relevant snippets from the JWA spec:


4.1. "alg" (Algorithm) Header Parameter Values for JWE

alg Parameter Value Key Encryption or Agreement Algorithm
ECDH-ES Elliptic Curve Diffie-Hellman Ephemeral Static, as defined in RFC 6090 , and using the Concat KDF, as defined in Section 5.8.1 of NIST.800-56A, where the Digest Method is SHA-256 and all OtherInfo parameters are the empty bit string


4.6. Key Agreement with Elliptic Curve Diffie-Hellman Ephemeral Static (ECDH-ES)


This section defines the specifics of agreeing upon a JWE CMK with Elliptic Curve Diffie-Hellman Ephemeral Static, as defined in RFC 6090, and using the Concat KDF, as defined in Section 5.8.1 of NIST.800-56A, where the Digest Method is SHA-256 and all OtherInfo parameters are the empty bit string. The alg header parameter value ECDH-ES is used in this case. A key of size 160 bits or larger MUST be used for the Elliptic Curve keys used with this algorithm. The output of the Concat KDF MUST be a key of the same length as that used by the enc algorithm. An epk (ephemeral public key) value MUST only be used for a single key agreement transaction.

Appendix B. Encryption Algorithm Identifier Cross-Reference
Algorithm JWE XML ENC JCA
Elliptic Curve Diffie-Hellman Ephemeral Static ECDH-ES http://www.w3.org/2009/xmlenc11#ECDH-ES TBD


I could not find a Java implementation in JavaSE and the Bouncycastle library does not seem to have one neither. Bouncycastle does implement keyderivation functions but not the one from NIST.800-56A. Valuable input came from this webpage "Key Derivation Functions: How many KDFs are there?". Taking the Bouncycasle implementation and converting it into KDFconcat is easy and here it is: https://code.google.com/p/openinfocard/source/browse/trunk/src/org/xmldap/crypto/KDFConcatGenerator.java

The next thing needed are some keypairs for the JUNIT test cases. I generated them using openssl.
openssl ecparam -out key1.pem -name secp256r1 -genkey
and displayed them using
 openssl ec -in key1.pem -text
read EC key
Private-Key: (256 bit)
priv:
    07:2f:23:22:c0:e7:5e:0c:85:17:64:b4:21:81:99:
    67:78:fd:22:59:2f:87:e5:d4:38:36:09:74:29:a1:
    c3:fc
pub:
    04:ed:3c:83:1b:f3:e1:05:9f:12:07:7f:4b:e4:fd:
    fe:90:55:73:d1:c6:76:45:b4:7d:48:64:ea:17:9d:
    de:99:86:a9:a6:ad:34:27:4a:80:fc:94:b3:a5:ef:
    6c:6e:78:2c:22:7a:39:63:a6:a4:26:50:97:6d:a6:
    ad:e9:90:a1:61
ASN1 OID: prime256v1
writing EC key
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIAcvIyLA514MhRdktCGBmWd4/SJZL4fl1Dg2CXQpocP8oAoGCCqGSM49
AwEHoUQDQgAE7TyDG/PhBZ8SB39L5P3+kFVz0cZ2RbR9SGTqF53emYappq00J0qA
/JSzpe9sbngsIno5Y6akJlCXbaat6ZChYQ==
-----END EC PRIVATE KEY-----
Too bad that the man page does not go into detail in what format priv and pub are... Read the source, Luke! It seems that the priv key D is just the bytes in hex of the private key BigInteger. The public key seems to be something else but this is no problem because in ECC the public key is G*D where G is a curve parameter.
So the two private keys are now defined here in the JUNIT tests. One is for the sender of the JWE the other for the recipient.
  static final String ec256_a_priv = "072f2322c0e75e0c851764b42181996778fd22592f87e5d43836097429a1c3fc";
  static final String ec256_b_priv = "1a3eda89dc067871530601f934c6428574f837507c578e45bd10a29b2e019bfb";
Now the public keys are computed like this:
  ASN1ObjectIdentifier oid = ECUtil.getNamedCurveOid("secp256r1");
  X9ECParameters x9ECParameters = ECUtil.getNamedCurveByOid(oid);
  byte[] ec256_a_priv_bytes = Hex.decode(ec256_a_priv);
  ec256_a_D = new BigInteger(1, ec256_a_priv_bytes);
  ECPoint pub = x9ECParameters.getG().multiply(ec256_a_D);
  ec256_a_X = pub.getX().toBigInteger();
  ec256_a_Y = pub.getY().toBigInteger();
This gives the following jwtHeader for the first private key: ECDH-ES jwtHeaderSegment:
{"alg":"ECDH-ES",
"enc":"A256GCM",
"iv":"__79_Pv6-fg",
"crv":"secp256r1",
"x":"AO08gxvz4QWfEgd_S-T9_pBVc9HGdkW0fUhk6hed3pmG",
"y":"AKmmrTQnSoD8lLOl72xueCwiejljpqQmUJdtpq3pkKFh"}
The spec says that the senders key pair is ephemeral and the recipient's key pair is static. So code using this code should generate one ephemeral EC key pair. Stuff the private key and the recipients public key into the encrypt function. These key parts are used for the key agreement. See the wikipedia page for a short explanation. I will implement the "epk" header parameter at a later time. For now I use the X and Y format to transfer the ephemeral public key to the recipient.
Base64 encoding the header yields:
eyJhbGciOiJFQ0RILUVTIiwNCiJlbmMiOiJBMjU2R0NNIiwNCiJpdiI6Il9fNzlfUHY2LWZnIiwNCiJjcnYiOiJzZWNwMjU2cjEiLA0KIngiOiJBTzA4Z3h2ejRRV2ZFZ2RfUy1UOV9wQlZjOUhHZGtXMGZVaGs2aGVkM3BtRyIsDQoieSI6IkFLbW1yVFFuU29EOGxMT2w3Mnh1ZUN3aWVqbGpwcVFtVUpkdHBxM3BrS0ZoIn0


The next step in my interpretation of the spec is to generate the content encryption key using the key derivation function defined in the NIST paper. The content encryption is done using the method specified in the enc parameter of the header here: A256GCM
For this method I need a 256bit == 32byte key and a 12 byte IV. So the call to the KDF is:
  ECDHBasicAgreement ecdhBasicAgreement = new ECDHBasicAgreement();
  ecdhBasicAgreement.init(ecPrivateKeyParameters);
  BigInteger z = ecdhBasicAgreement.calculateAgreement(ecPublicKeyParameters);
  byte[] zBytes = BigIntegers.asUnsignedByteArray(z);
  KDFConcatGenerator kdfConcatGenerator = new KDFConcatGenerator(kdfDigest);
  kdfConcatGenerator.init(new KDFParameters(zBytes, null));
  byte[] out = new byte[12 + (keylength / 8)];
  kdfConcatGenerator.generateBytes(out, 0, out.length);
  byte[] secretKeyBytes = new byte[keylength / 8];
  byte[] ivBytes = new byte[12];
  System.arraycopy(out, 0, ivBytes, 0, 12);
  System.arraycopy(out, 12, secretKeyBytes, 0, secretKeyBytes.length);

Now a random 256bit AES key is generated and encrypted using the secret key and IV generated by the KDF.
For this content encryption key (base64url)
ECDH-ES contentEncryptionKey=Y8-fcu11np1l3qlgpGq0XF58Cv2n4DOJ8lkdl2gRXgI
the values of the key segment and the jwe crypto segment are this:
ECDH-ES jwtSymmetricKeySegment base64: _vZ-N5fs3_uJ9o-woDOzKZBQopZRi4EfZVNB4UWqdCjappfoOFCZPTUBDruWAtWY
ECDH-ES jwtCryptoSegment base64: psSi2xa7oY1pmK1m9GGXREr9YB6QUdOtK_Jl_nnKYpmKGHL577tUdadK8H_yacb78bBlyTnrTx51pmxyo6UJMM9c_P-lOfMiMslvS-3t1vD5HiOq0Rg
Please use the IETF JOSE mailing list for comments and suggestions.

Special thanks to Mike Jones for writing most of the JW* specs text and to John Bradley for being a fountain of knowledge in all things crypto and identity management protocols and formats. Not to forget Nat Sakimura for starting the OpenID Artifact Binding WG which now does all the OpenID Connect work. Especially for mobile devices we need simple, light weight protocols and formats. JW* and especially ECDH-ES are important for mobile.