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.