How to serialize and consume ECDH parameters in Java

2.9k views Asked by At

I am looking to perform ECDH between 2 platforms to derive a shared secret. I am planning to use a named curve (which curve not yet determined). The flow would look something like this:

  • Alice picks a curve
  • Alice generates a random key pair for her curve
  • Alice serializes some data about her curve
  • Alice sends her public key and curve data to Bob
  • Bob initializes a curve with Alice's data
  • Bob creates a key pair based on Alice's data
  • Bob performs ECDH to derive shared secret
  • Bob responds to Alice with his public key
  • Alice performs ECDH to derive shared secret

Using bouncycastle, what is the cleanest way to accomplish this?

Almost all examples I've seen (like this one: https://gist.github.com/wuyongzheng/0e2ed6d8a075153efcd3) illustrate the process of arriving at the shared secret, but none seem to factor in the process of actually serializing information about the curve / starting point (G) to "Bob" and how to use that data on Bob's side to rebuild the curve and generate corresponding keys. What data would you need to send to Bob?

2

There are 2 answers

1
nak5ive On

I believe i found a relatively succinct way of demonstrating the problem / solution for this. I was originally missing the fact that named curves include a common starting point, so no need to serialize that data if you have an agreed-upon curve.

Security.addProvider(new BouncyCastleProvider());

// Alice sets up the exchange
KeyPairGenerator aliceKeyGen = KeyPairGenerator.getInstance("ECDH", "BC");
aliceKeyGen.initialize(new ECGenParameterSpec("prime256v1"), new SecureRandom());

KeyPair alicePair = aliceKeyGen.generateKeyPair();
ECPublicKey alicePub = (ECPublicKey)alicePair.getPublic();
ECPrivateKey alicePvt = (ECPrivateKey)alicePair.getPrivate();

byte[] alicePubEncoded = alicePub.getEncoded();
byte[] alicePvtEncoded = alicePvt.getEncoded();

System.out.println("Alice public: " + DatatypeConverter.printHexBinary(alicePubEncoded));
System.out.println("Alice private: " + DatatypeConverter.printHexBinary(alicePvtEncoded));


// POST hex(alicePubEncoded)

// Bob receives Alice's public key

KeyFactory kf = KeyFactory.getInstance("EC");
PublicKey remoteAlicePub = kf.generatePublic(new X509EncodedKeySpec(alicePubEncoded));

KeyPairGenerator bobKeyGen = KeyPairGenerator.getInstance("ECDH", "BC");
bobKeyGen.initialize(new ECGenParameterSpec("prime256v1"), new SecureRandom());

KeyPair bobPair = bobKeyGen.generateKeyPair();
ECPublicKey bobPub = (ECPublicKey)bobPair.getPublic();
ECPrivateKey bobPvt = (ECPrivateKey)bobPair.getPrivate();

byte[] bobPubEncoded = bobPub.getEncoded();
byte[] bobPvtEncoded = bobPvt.getEncoded();

System.out.println("Bob public: " + DatatypeConverter.printHexBinary(bobPubEncoded));
System.out.println("Bob private: " + DatatypeConverter.printHexBinary(bobPvtEncoded));

KeyAgreement bobKeyAgree = KeyAgreement.getInstance("ECDH");
bobKeyAgree.init(bobPvt);
bobKeyAgree.doPhase(remoteAlicePub, true);

System.out.println("Bob secret: " + DatatypeConverter.printHexBinary(bobKeyAgree.generateSecret()));


// RESPOND hex(bobPubEncoded)

// Alice derives secret

KeyFactory aliceKf = KeyFactory.getInstance("EC");
PublicKey remoteBobPub = aliceKf.generatePublic(new X509EncodedKeySpec(bobPubEncoded));

KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("ECDH");
aliceKeyAgree.init(alicePvt);
aliceKeyAgree.doPhase(remoteBobPub, true);

System.out.println("Alice secret: " + DatatypeConverter.printHexBinary(aliceKeyAgree.generateSecret()));

And on first run, this yielded:

Alice public: 3059301306072A8648CE3D020106082A8648CE3D03010703420004D8FF8DAB9683C7D6C798FE381775AE3BCC25260E2B270C57584F684BFBF59A73221480040E70993F2F4DEBE25A19E772896F5C98DFAE6865C31830BBD876E8DF
Alice private: 308193020100301306072A8648CE3D020106082A8648CE3D030107047930770201010420A08DEC852618FA6BF0CA8B67DFFCC72AA39BE7402978CA456F73660337837DE1A00A06082A8648CE3D030107A14403420004D8FF8DAB9683C7D6C798FE381775AE3BCC25260E2B270C57584F684BFBF59A73221480040E70993F2F4DEBE25A19E772896F5C98DFAE6865C31830BBD876E8DF
Bob public: 3059301306072A8648CE3D020106082A8648CE3D03010703420004E4343FD573F117446925BBFE0DEF591098AA300066AB4F51DC2736736C8CE18BA72EA67AE4D0D6DD5E22007BA45BAA9DCE473002D17D6A29207AA15A1E97C596
Bob private: 308193020100301306072A8648CE3D020106082A8648CE3D030107047930770201010420D272E7BD59F7EA2AA3710910073AFE58082BC460B347A3782981CCCABA452538A00A06082A8648CE3D030107A14403420004E4343FD573F117446925BBFE0DEF591098AA300066AB4F51DC2736736C8CE18BA72EA67AE4D0D6DD5E22007BA45BAA9DCE473002D17D6A29207AA15A1E97C596
Bob secret: B65B4C8A1C797B867CE39F26DC97A0241A407FC79CF0D3CBA061A4A907CF3E1B
Alice secret: B65B4C8A1C797B867CE39F26DC97A0241A407FC79CF0D3CBA061A4A907CF3E1B
0
dave_thompson_085 On

You don't necessarily need to send the curve, you can fix it in advance. As an important example Bitcoin, which uses ECDSA rather than ECDH, specifies secp256k1.

However, the code in your answer uses the encodings returned by Java PublicKey.getEncoded() and PrivateKey.getEncoded() which are 'X.509' (more precisely the SubjectPublicKeyInfo structure in X.509) and 'PKCS8' respectively; see the javadoc. These are both ASN.1 encodings that include an AlgorithmIdentifier containing parameters which for ECC define the curve either by an ASN.1 OBJECT IDENTIFIER aka OID or by a detailed specification of the underlying field, coefficients for the Weierstrass curve equation, base point, order and cofactor. In practice everyone uses the standard named curves which have OIDs. That is why 21 bytes beginning at offset 2 in all of your key printouts are identical; it's an ASN.1 SEQUENCE containing an OID for the algorithm (id-ecPublicKey) and an OID for the selected curve (prime256v1).

Other schemes are possible. TLS ECDHE sends either the curve details or a small integer identifying a standard curve as defined in rfc4492 of which the latter is almost always used; static-ECDH uses X.509 certs, and thus the X.509 format. SSH ECDH ephemeral or ECMQV sends a string containing the name or the OID, see rfc5656. CMS and thus S/MIME uses ASN.1 structures containing the AlgorithmIdentifier with OID-form namedCurve parameters, see rfc 5753.