Bug 956816 - Implement (de)serializing Android BrowserIDKeyPair instances. r=rnewman

This commit is contained in:
Nick Alexander 2014-01-07 18:27:45 -08:00
parent 39b8f0d06d
commit f6fa542334
9 changed files with 195 additions and 37 deletions

View File

@ -4,7 +4,12 @@
package org.mozilla.gecko.browserid;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class BrowserIDKeyPair {
public static final String JSON_KEY_PRIVATEKEY = "privateKey";
public static final String JSON_KEY_PUBLICKEY = "publicKey";
protected final SigningPrivateKey privateKey;
protected final VerifyingPublicKey publicKey;
@ -20,4 +25,11 @@ public class BrowserIDKeyPair {
public VerifyingPublicKey getPublic() {
return this.publicKey;
}
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put(JSON_KEY_PRIVATEKEY, privateKey.toJSONObject());
o.put(JSON_KEY_PUBLICKEY, publicKey.toJSONObject());
return o;
}
}

View File

@ -20,12 +20,21 @@ import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
public class DSACryptoImplementation {
public static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long.
/**
* Parameters are serialized as hex strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted. We
* expect to follow the JOSE/JWT spec as it solidifies, and that will probably
* mean unifying this base.
*/
protected static final int SERIALIZATION_BASE = 16;
protected static class DSAVerifyingPublicKey implements VerifyingPublicKey {
protected final DSAPublicKey publicKey;
@ -33,16 +42,22 @@ public class DSACryptoImplementation {
this.publicKey = publicKey;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as hex strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public String serialize() {
public ExtendedJSONObject toJSONObject() {
DSAParams params = publicKey.getParams();
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "DS");
o.put("y", publicKey.getY().toString(16));
o.put("g", params.getG().toString(16));
o.put("p", params.getP().toString(16));
o.put("q", params.getQ().toString(16));
return o.toJSONString();
o.put("y", publicKey.getY().toString(SERIALIZATION_BASE));
o.put("g", params.getG().toString(SERIALIZATION_BASE));
o.put("p", params.getP().toString(SERIALIZATION_BASE));
o.put("q", params.getQ().toString(SERIALIZATION_BASE));
return o;
}
@Override
@ -87,16 +102,22 @@ public class DSACryptoImplementation {
return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public String serialize() {
public ExtendedJSONObject toJSONObject() {
DSAParams params = privateKey.getParams();
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "DS");
o.put("x", privateKey.getX().toString(16));
o.put("g", params.getG().toString(16));
o.put("p", params.getP().toString(16));
o.put("q", params.getQ().toString(16));
return o.toJSONString();
o.put("x", privateKey.getX().toString(SERIALIZATION_BASE));
o.put("g", params.getG().toString(SERIALIZATION_BASE));
o.put("p", params.getP().toString(SERIALIZATION_BASE));
o.put("q", params.getQ().toString(SERIALIZATION_BASE));
return o;
}
@Override
@ -121,7 +142,7 @@ public class DSACryptoImplementation {
}
}
public static BrowserIDKeyPair generateKeypair(int keysize)
public static BrowserIDKeyPair generateKeyPair(int keysize)
throws NoSuchAlgorithmException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(keysize);
@ -168,4 +189,56 @@ public class DSACryptoImplementation {
DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec);
return new DSAVerifyingPublicKey(publicKey);
}
public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"DS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
}
try {
BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE);
BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
return createPrivateKey(x, p, q, g);
} catch (NullPointerException e) {
throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
} catch (NumberFormatException e) {
throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"DS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
}
try {
BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE);
BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
return createPublicKey(y, p, q, g);
} catch (NullPointerException e) {
throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
} catch (NumberFormatException e) {
throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
if (privateKey == null) {
throw new InvalidKeySpecException("privateKey must not be null");
}
if (publicKey == null) {
throw new InvalidKeySpecException("publicKey must not be null");
}
return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
} catch (NonObjectJSONException e) {
throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
}
}
}

View File

@ -93,7 +93,7 @@ public class JSONWebTokenUtils {
ExtendedJSONObject principal = new ExtendedJSONObject();
principal.put("email", email);
payload.put("principal", principal);
payload.put("public-key", new ExtendedJSONObject(publicKeyToSign.serialize()));
payload.put("public-key", publicKeyToSign.toJSONObject());
return payload.toJSONString();
}

View File

@ -19,10 +19,19 @@ import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
public class RSACryptoImplementation {
public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted. We
* expect to follow the JOSE/JWT spec as it solidifies, and that will probably
* mean unifying this base.
*/
protected static final int SERIALIZATION_BASE = 10;
protected static class RSAVerifyingPublicKey implements VerifyingPublicKey {
protected final RSAPublicKey publicKey;
@ -30,13 +39,19 @@ public class RSACryptoImplementation {
this.publicKey = publicKey;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public String serialize() {
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "RS");
o.put("n", publicKey.getModulus().toString(10));
o.put("e", publicKey.getPublicExponent().toString(10));
return o.toJSONString();
o.put("n", publicKey.getModulus().toString(SERIALIZATION_BASE));
o.put("e", publicKey.getPublicExponent().toString(SERIALIZATION_BASE));
return o;
}
@Override
@ -61,13 +76,19 @@ public class RSACryptoImplementation {
return "RS" + (privateKey.getModulus().bitLength() + 7)/8;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public String serialize() {
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "RS");
o.put("n", privateKey.getModulus().toString(10));
o.put("e", privateKey.getPrivateExponent().toString(10));
return o.toJSONString();
o.put("n", privateKey.getModulus().toString(SERIALIZATION_BASE));
o.put("d", privateKey.getPrivateExponent().toString(SERIALIZATION_BASE));
return o;
}
@Override
@ -80,7 +101,7 @@ public class RSACryptoImplementation {
}
}
public static BrowserIDKeyPair generateKeypair(final int keysize) throws NoSuchAlgorithmException {
public static BrowserIDKeyPair generateKeyPair(final int keysize) throws NoSuchAlgorithmException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(keysize);
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
@ -114,4 +135,52 @@ public class RSACryptoImplementation {
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
return new RSAVerifyingPublicKey(publicKey);
}
public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"RS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
}
try {
BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
BigInteger d = new BigInteger(o.getString("d"), SERIALIZATION_BASE);
return createPrivateKey(n, d);
} catch (NullPointerException e) {
throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE);
} catch (NumberFormatException e) {
throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"RS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
}
try {
BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
BigInteger e = new BigInteger(o.getString("e"), SERIALIZATION_BASE);
return createPublicKey(n, e);
} catch (NullPointerException e) {
throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE);
} catch (NumberFormatException e) {
throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
if (privateKey == null) {
throw new InvalidKeySpecException("privateKey must not be null");
}
if (publicKey == null) {
throw new InvalidKeySpecException("publicKey must not be null");
}
return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
} catch (NonObjectJSONException e) {
throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
}
}
}

View File

@ -6,6 +6,8 @@ package org.mozilla.gecko.browserid;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface SigningPrivateKey {
/**
* Return the JSON Web Token "alg" header corresponding to this private key.
@ -18,16 +20,16 @@ public interface SigningPrivateKey {
public String getAlgorithm();
/**
* Generate a printable representation of a private key.
* Generate a JSON representation of a private key.
* <p>
* <b>This should only be used for debugging. No private keys should go over
* the wire at any time.</b>
*
* @param privateKey
* to represent.
* @return printable representation.
* @return JSON representation.
*/
public String serialize();
public ExtendedJSONObject toJSONObject();
/**
* Sign a message.

View File

@ -6,16 +6,18 @@ package org.mozilla.gecko.browserid;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface VerifyingPublicKey {
/**
* Generate a printable representation of a public key.
* Generate a JSON representation of a public key.
*
* @param publicKey
* to represent.
* @return printable representation.
* @return JSON representation.
*/
public String serialize();
public ExtendedJSONObject toJSONObject();
/**
* Verify a signature.

View File

@ -155,9 +155,9 @@ public class FxAccount {
*/
public void login(final Context context, final String tokenServerEndpoint,
final BrowserIDKeyPair keyPair, final Delegate delegate) {
ExtendedJSONObject keyPairObject;
ExtendedJSONObject publicKeyObject;
try {
keyPairObject = new ExtendedJSONObject(keyPair.getPublic().serialize());
publicKeyObject = keyPair.getPublic().toJSONObject();
} catch (Exception e) {
delegate.handleError(e);
return;
@ -168,7 +168,7 @@ public class FxAccount {
// inner FxAccountClient delegate, the outer TokenServerClient delegate, and
// the user supplied delegate.
FxAccountClient fxAccountClient = new FxAccountClient(idpEndpoint, executor);
fxAccountClient.sign(sessionTokenBytes, keyPairObject,
fxAccountClient.sign(sessionTokenBytes, publicKeyObject,
JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS,
new InnerFxAccountClientRequestDelegate(executor, authEndpoint, tokenServerEndpoint, keyPair, delegate));
}

View File

@ -113,7 +113,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
final CountDownLatch latch = new CountDownLatch(1);
try {
final BrowserIDKeyPair keyPair = RSACryptoImplementation.generateKeypair(1024);
final BrowserIDKeyPair keyPair = RSACryptoImplementation.generateKeyPair(1024);
Logger.info(LOG_TAG, "Generated keypair. ");
final FxAccount fxAccount = FxAccountAuthenticator.fromAndroidAccount(getContext(), account);

View File

@ -38,12 +38,12 @@ public class TestBrowserIDKeyPairGeneration extends AndroidSyncTestCase {
}
public void testEncodeDecodeSuccessRSA() throws Exception {
doTestEncodeDecode(RSACryptoImplementation.generateKeypair(1024));
doTestEncodeDecode(RSACryptoImplementation.generateKeypair(2048));
doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(1024));
doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(2048));
}
public void testEncodeDecodeSuccessDSA() throws Exception {
doTestEncodeDecode(DSACryptoImplementation.generateKeypair(512));
doTestEncodeDecode(DSACryptoImplementation.generateKeypair(1024));
doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(512));
doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(1024));
}
}