diff --git a/core/java/src/net/i2p/crypto/DSAEngine.java b/core/java/src/net/i2p/crypto/DSAEngine.java index e59e56343568a175ae96967313d97d2b109ac8f1..0d47954c04222bd2292caeab24459989601ab672 100644 --- a/core/java/src/net/i2p/crypto/DSAEngine.java +++ b/core/java/src/net/i2p/crypto/DSAEngine.java @@ -29,10 +29,18 @@ package net.i2p.crypto; * POSSIBILITY OF SUCH DAMAGE. */ +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory;; import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.KeySpec; import net.i2p.I2PAppContext; import net.i2p.data.Hash; @@ -44,26 +52,67 @@ import net.i2p.util.Log; import net.i2p.util.NativeBigInteger; /** + * Sign and verify using DSA-SHA1. + * Also contains methods to sign and verify using a SHA-256 Hash, used by Syndie only. + * + * The primary implementation is code from TheCryto. + * As of 0.8.7, also included is an alternate implementation using java.security libraries, which + * is slightly slower. This implementation could in the future be easily modified + * to use a new signing algorithm from java.security when we change the signing algorithm. + * * Params and rv's changed from Hash to SHA1Hash for version 0.8.1 * Hash variants of sign() and verifySignature() restored in 0.8.3, required by Syndie. */ public class DSAEngine { - private Log _log; - private I2PAppContext _context; + private final Log _log; + private final I2PAppContext _context; + + //private static final boolean _isAndroid = System.getProperty("java.vendor").contains("Android"); + private static final boolean _useJavaLibs = false; // = _isAndroid; public DSAEngine(I2PAppContext context) { _log = context.logManager().getLog(DSAEngine.class); _context = context; } + public static DSAEngine getInstance() { return I2PAppContext.getGlobalContext().dsa(); } + + /** + * Verify using DSA-SHA1. + * Uses TheCrypto code unless configured to use the java.security libraries. + */ public boolean verifySignature(Signature signature, byte signedData[], SigningPublicKey verifyingKey) { - return verifySignature(signature, signedData, 0, signedData.length, verifyingKey); + boolean rv; + if (_useJavaLibs) { + try { + rv = altVerifySigSHA1(signature, signedData, verifyingKey); + if ((!rv) && _log.shouldLog(Log.WARN)) + _log.warn("Lib Verify Fail, sig =\n" + signature + "\npubkey =\n" + verifyingKey); + return rv; + } catch (GeneralSecurityException gse) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Lib Verify Fail, sig =\n" + signature + "\npubkey =\n" + verifyingKey, gse); + // now try TheCrypto + } + } + rv = verifySignature(signature, signedData, 0, signedData.length, verifyingKey); + if ((!rv) && _log.shouldLog(Log.WARN)) + _log.warn("TheCrypto Verify Fail, sig =\n" + signature + "\npubkey =\n" + verifyingKey); + return rv; } + + /** + * Verify using DSA-SHA1 + */ public boolean verifySignature(Signature signature, byte signedData[], int offset, int size, SigningPublicKey verifyingKey) { return verifySignature(signature, calculateHash(signedData, offset, size), verifyingKey); } + + /** + * Verify using DSA-SHA1 + */ public boolean verifySignature(Signature signature, InputStream in, SigningPublicKey verifyingKey) { return verifySignature(signature, calculateHash(in), verifyingKey); } @@ -92,6 +141,8 @@ public class DSAEngine { byte[] sigbytes = signature.getData(); byte rbytes[] = new byte[20]; byte sbytes[] = new byte[20]; + //System.arraycopy(sigbytes, 0, rbytes, 0, 20); + //System.arraycopy(sigbytes, 20, sbytes, 0, 20); for (int x = 0; x < 40; x++) { if (x < 20) { rbytes[x] = sigbytes[x]; @@ -99,6 +150,7 @@ public class DSAEngine { sbytes[x - 20] = sigbytes[x]; } } + BigInteger s = new NativeBigInteger(1, sbytes); BigInteger r = new NativeBigInteger(1, rbytes); BigInteger y = new NativeBigInteger(1, verifyingKey.getData()); @@ -106,6 +158,7 @@ public class DSAEngine { try { w = s.modInverse(CryptoConstants.dsaq); } catch (ArithmeticException ae) { + _log.warn("modInverse() error", ae); return false; } byte data[] = hash.getData(); @@ -130,15 +183,36 @@ public class DSAEngine { } } + /** + * Sign using DSA-SHA1. + * Uses TheCrypto code unless configured to use the java.security libraries. + */ public Signature sign(byte data[], SigningPrivateKey signingKey) { + if (_useJavaLibs) { + try { + return altSignSHA1(data, signingKey); + } catch (GeneralSecurityException gse) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Lib Sign Fail, privkey = " + signingKey, gse); + // now try TheCrypto + } + } return sign(data, 0, data.length, signingKey); } + + /** + * Sign using DSA-SHA1 + */ public Signature sign(byte data[], int offset, int length, SigningPrivateKey signingKey) { if ((signingKey == null) || (data == null) || (data.length <= 0)) return null; SHA1Hash h = calculateHash(data, offset, length); return sign(h, signingKey); } + /** + * Sign using DSA-SHA1. + * Reads the stream until EOF. Does not close the stream. + */ public Signature sign(InputStream in, SigningPrivateKey signingKey) { if ((signingKey == null) || (in == null) ) return null; SHA1Hash h = calculateHash(in); @@ -192,28 +266,34 @@ public class DSAEngine { _context.random().harvester().feedEntropy("DSA.sign", rbytes, 0, rbytes.length); if (rbytes.length == 20) { + //System.arraycopy(rbytes, 0, out, 0, 20); for (int i = 0; i < 20; i++) { out[i] = rbytes[i]; } } else if (rbytes.length == 21) { + //System.arraycopy(rbytes, 1, out, 0, 20); for (int i = 0; i < 20; i++) { out[i] = rbytes[i + 1]; } } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Using short rbytes.length [" + rbytes.length + "]"); + //System.arraycopy(rbytes, 0, out, 20 - rbytes.length, rbytes.length); for (int i = 0; i < rbytes.length; i++) out[i + 20 - rbytes.length] = rbytes[i]; } if (sbytes.length == 20) { + //System.arraycopy(sbytes, 0, out, 20, 20); for (int i = 0; i < 20; i++) { out[i + 20] = sbytes[i]; } } else if (sbytes.length == 21) { + //System.arraycopy(sbytes, 1, out, 20, 20); for (int i = 0; i < 20; i++) { out[i + 20] = sbytes[i + 1]; } } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Using short sbytes.length [" + sbytes.length + "]"); + //System.arraycopy(sbytes, 0, out, 40 - sbytes.length, sbytes.length); for (int i = 0; i < sbytes.length; i++) out[i + 20 + 20 - sbytes.length] = sbytes[i]; } @@ -227,7 +307,11 @@ public class DSAEngine { return sig; } - /** @return hash SHA-1 hash, NOT a SHA-256 hash */ + /** + * Reads the stream until EOF. Does not close the stream. + * + * @return hash SHA-1 hash, NOT a SHA-256 hash + */ public SHA1Hash calculateHash(InputStream in) { MessageDigest digest = SHA1.getInstance(); byte buf[] = new byte[64]; @@ -252,18 +336,222 @@ public class DSAEngine { return new SHA1Hash(digested); } + /** + * Alternate to verifySignature() using java.security libraries. + * @throws GeneralSecurityException if algorithm unvailable or on other errors + * @since 0.8.7 + */ + private boolean altVerifySigSHA1(Signature signature, byte[] data, SigningPublicKey verifyingKey) throws GeneralSecurityException { + java.security.Signature jsig = java.security.Signature.getInstance("SHA1withDSA"); + KeyFactory keyFact = KeyFactory.getInstance("DSA"); + // y p q g + KeySpec spec = new DSAPublicKeySpec(new NativeBigInteger(1, verifyingKey.getData()), + CryptoConstants.dsap, + CryptoConstants.dsaq, + CryptoConstants.dsag); + PublicKey pubKey = keyFact.generatePublic(spec); + jsig.initVerify(pubKey); + jsig.update(data); + boolean rv = jsig.verify(sigBytesToASN1(signature.getData())); + //if (!rv) { + // System.out.println("BAD SIG\n" + net.i2p.util.HexDump.dump(signature.getData())); + // System.out.println("BAD SIG\n" + net.i2p.util.HexDump.dump(sigBytesToASN1(signature.getData()))); + //} + return rv; + } + + /** + * Alternate to sign() using java.security libraries. + * @throws GeneralSecurityException if algorithm unvailable or on other errors + * @since 0.8.7 + */ + private Signature altSignSHA1(byte[] data, SigningPrivateKey privateKey) throws GeneralSecurityException { + java.security.Signature jsig = java.security.Signature.getInstance("SHA1withDSA"); + KeyFactory keyFact = KeyFactory.getInstance("DSA"); + // y p q g + KeySpec spec = new DSAPrivateKeySpec(new NativeBigInteger(1, privateKey.getData()), + CryptoConstants.dsap, + CryptoConstants.dsaq, + CryptoConstants.dsag); + PrivateKey privKey = keyFact.generatePrivate(spec); + jsig.initSign(privKey, _context.random()); + jsig.update(data); + return new Signature(aSN1ToSigBytes(jsig.sign())); + } + + /** + * http://download.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html + * Signature Format ASN.1 sequence of two INTEGER values: r and s, in that order: + * SEQUENCE ::= { r INTEGER, s INTEGER } + * + * http://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One + * 30 -- tag indicating SEQUENCE + * xx - length in octets + * + * 02 -- tag indicating INTEGER + * xx - length in octets + * xxxxxx - value + * + * Convert to BigInteger and back so we have the minimum length representation, as required. + * r and s are always non-negative. + * + * @since 0.8.7 + */ + private static byte[] sigBytesToASN1(byte[] sig) { + //System.out.println("pre TO asn1\n" + net.i2p.util.HexDump.dump(sig)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(48); + baos.write(0x30); + baos.write(0); // length to be filled in below + + byte[] tmp = new byte[20]; + baos.write(2); + System.arraycopy(sig, 0, tmp, 0, 20); + BigInteger r = new BigInteger(1, tmp); + byte[] b = r.toByteArray(); + baos.write(b.length); + baos.write(b, 0, b.length); + + baos.write(2); + System.arraycopy(sig, 20, tmp, 0, 20); + BigInteger s = new BigInteger(1, tmp); + b = s.toByteArray(); + baos.write(b.length); + baos.write(b, 0, b.length); + byte[] rv = baos.toByteArray(); + rv[1] = (byte) (rv.length - 2); + //System.out.println("post TO asn1\n" + net.i2p.util.HexDump.dump(rv)); + return rv; + } + + /** + * See above. + * @since 0.8.7 + */ + private static byte[] aSN1ToSigBytes(byte[] asn) { + //System.out.println("pre from asn1\n" + net.i2p.util.HexDump.dump(asn)); + byte[] rv = new byte[40]; + int rlen = asn[3]; + if ((asn[4] & 0x80) != 0) + throw new IllegalArgumentException("R is negative"); + if (rlen > 21) + throw new IllegalArgumentException("R too big " + rlen); + else if (rlen == 21) { + System.arraycopy(asn, 5, rv, 0, 20); + } else + System.arraycopy(asn, 4, rv, 20 - rlen, rlen); + int slenloc = 25 + rlen - 20; + int slen = asn[slenloc]; + if ((asn[slenloc + 1] & 0x80) != 0) + throw new IllegalArgumentException("S is negative"); + if (slen > 21) + throw new IllegalArgumentException("S too big " + slen); + else if (slen == 21) { + System.arraycopy(asn, slenloc + 2, rv, 20, 20); + } else + System.arraycopy(asn, slenloc + 1, rv, 40 - slen, slen); + //System.out.println("post from asn1\n" + net.i2p.util.HexDump.dump(rv)); + return rv; + } + + private static final int RUNS = 1000; + + /** + * Run consistency and speed tests with both TheCrypto and java.security libraries. + * + * TheCrypto is about 5-15% faster than java.security. + */ public static void main(String args[]) { I2PAppContext ctx = I2PAppContext.getGlobalContext(); - byte data[] = new byte[4096]; + byte data[] = new byte[1024]; + // warmump ctx.random().nextBytes(data); - Object keys[] = ctx.keyGenerator().generateSigningKeypair(); try { - for (int i = 0; i < 10; i++) { - Signature sig = ctx.dsa().sign(data, (SigningPrivateKey)keys[1]); - boolean ok = ctx.dsa().verifySignature(sig, data, (SigningPublicKey)keys[0]); - System.out.println("OK: " + ok); + Thread.sleep(1000); + } catch (InterruptedException ie) {} + SimpleDataStructure keys[] = null; + + System.err.println("100 runs with new data and keys each time"); + for (int i = 0; i < 100; i++) { + ctx.random().nextBytes(data); + keys = ctx.keyGenerator().generateSigningKeys(); + Signature sig = ctx.dsa().sign(data, (SigningPrivateKey)keys[1]); + Signature jsig = null; + try { + jsig = ctx.dsa().altSignSHA1(data, (SigningPrivateKey)keys[1]); + } catch (GeneralSecurityException gse) { + gse.printStackTrace(); } - } catch (Exception e) { e.printStackTrace(); } - ctx.random().saveSeed(); - } + boolean ok = ctx.dsa().verifySignature(jsig, data, (SigningPublicKey)keys[0]); + boolean usok = ctx.dsa().verifySignature(sig, data, (SigningPublicKey)keys[0]); + boolean jok = false; + try { + jok = ctx.dsa().altVerifySigSHA1(sig, data, (SigningPublicKey)keys[0]); + } catch (GeneralSecurityException gse) { + gse.printStackTrace(); + } + boolean jjok = false;; + try { + jjok = ctx.dsa().altVerifySigSHA1(jsig, data, (SigningPublicKey)keys[0]); + } catch (GeneralSecurityException gse) { + gse.printStackTrace(); + } + System.err.println("TC->TC OK: " + usok + " JL->TC OK: " + ok + " TC->JK OK: " + jok + " JL->JL OK: " + jjok); + if (!(ok && usok && jok && jjok)) { + System.out.println("privkey\n" + net.i2p.util.HexDump.dump(keys[1].getData())); + return; + } + } + + System.err.println("Starting speed test"); + long start = System.currentTimeMillis(); + for (int i = 0; i < RUNS; i++) { + Signature sig = ctx.dsa().sign(data, (SigningPrivateKey)keys[1]); + boolean ok = ctx.dsa().verifySignature(sig, data, (SigningPublicKey)keys[0]); + if (!ok) { + System.err.println("TheCrypto FAIL"); + return; + } + } + long time = System.currentTimeMillis() - start; + System.err.println("Time for " + RUNS + " DSA sign/verifies:"); + System.err.println("TheCrypto time (ms): " + time); + + start = System.currentTimeMillis(); + for (int i = 0; i < RUNS; i++) { + boolean ok = false; + try { + Signature jsig = ctx.dsa().altSignSHA1(data, (SigningPrivateKey)keys[1]); + ok = ctx.dsa().altVerifySigSHA1(jsig, data, (SigningPublicKey)keys[0]); + } catch (GeneralSecurityException gse) { + gse.printStackTrace(); + } + if (!ok) { + System.err.println("JavaLib FAIL"); + return; + } + } + time = System.currentTimeMillis() - start; + System.err.println("JavaLib time (ms): " + time); + +/**** yes, arraycopy is slower for 20 bytes + start = System.currentTimeMillis(); + byte b[] = new byte[20]; + for (int i = 0; i < 10000000; i++) { + data[0] = data[i % 256]; + System.arraycopy(data, 0, b, 0, 20); + } + time = System.currentTimeMillis() - start; + System.err.println("arraycopy time (ms): " + time); + + start = System.currentTimeMillis(); + for (int i = 0; i < 10000000; i++) { + data[0] = data[i % 256]; + for (int j = 0; j < 20; j++) { + b[j] = data[j]; + } + } + time = System.currentTimeMillis() - start; + System.err.println("loop time (ms): " + time); +****/ + } }