From b36def1f7252e28c9f3222d9dc79535c0e533e8a Mon Sep 17 00:00:00 2001 From: smeghead <smeghead> Date: Fri, 8 Apr 2005 12:39:20 +0000 Subject: [PATCH] 2005-04-08 smeghead * Security improvements to TrustedUpdate: signing and verification of the version string along with the data payload for signed update files (consequently the positions of the DSA signature and version string fields have been swapped in the spec for the update file's header); router will no longer perform a trusted update if the signed update's version is lower than or equal to the currently running router's version. * Added two new CLI commands to TrustedUpdate: showversion, verifyupdate. * Extended TrustedUpdate public API for use by third party applications. --- .../src/net/i2p/router/web/NewsFetcher.java | 51 +- .../src/net/i2p/router/web/UpdateHandler.java | 20 +- .../src/net/i2p/crypto/TrustedUpdate.java | 741 ++++++++++++------ history.txt | 12 +- 4 files changed, 524 insertions(+), 300 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java index 057ad2ef40..0299156a20 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import net.i2p.crypto.TrustedUpdate; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.router.RouterVersion; @@ -136,7 +137,7 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { String ver = buf.substring(index+VERSION_PREFIX.length(), end); if (_log.shouldLog(Log.DEBUG)) _log.debug("Found version: [" + ver + "]"); - if (needsUpdate(ver)) { + if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Our version is out of date, update!"); break; @@ -191,54 +192,6 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { } } - private boolean needsUpdate(String version) { - StringTokenizer newTok = new StringTokenizer(sanitize(version), "."); - StringTokenizer ourTok = new StringTokenizer(sanitize(RouterVersion.VERSION), "."); - - while (newTok.hasMoreTokens() && ourTok.hasMoreTokens()) { - String newVer = newTok.nextToken(); - String oldVer = ourTok.nextToken(); - switch (compare(newVer, oldVer)) { - case -1: // newVer is smaller - return false; - case 0: // eq - break; - case 1: // newVer is larger - return true; - } - } - if (newTok.hasMoreTokens() && !ourTok.hasMoreTokens()) - return true; - return false; - } - - private static final String VALID = "0123456789."; - private static final String sanitize(String str) { - StringBuffer buf = new StringBuffer(str); - for (int i = 0; i < buf.length(); i++) { - if (VALID.indexOf(buf.charAt(i)) == -1) { - buf.deleteCharAt(i); - i--; - } - } - return buf.toString(); - } - - private static final int compare(String lhs, String rhs) { - try { - int left = Integer.parseInt(lhs); - int right = Integer.parseInt(rhs); - if (left < right) - return -1; - else if (left == right) - return 0; - else - return 1; - } catch (NumberFormatException nfe) { - return 0; - } - } - public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { // ignore } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java index c2fd838f5b..c412d025b3 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java @@ -2,19 +2,25 @@ package net.i2p.router.web; import java.io.File; import java.text.DecimalFormat; + import net.i2p.crypto.TrustedUpdate; import net.i2p.router.Router; import net.i2p.router.RouterContext; -import net.i2p.util.I2PThread; +import net.i2p.router.RouterVersion; import net.i2p.util.EepGet; +import net.i2p.util.I2PThread; import net.i2p.util.Log; /** - * Handle the request to update the router by firing off an EepGet call and - * displaying its status to anyone who asks. After the download completes, - * it is verified with the TrustedUpdate, and if it is authentic, the router - * is restarted. - * + * <p>Handles the request to update the router by firing off an + * {@link net.i2p.util.EepGet} call to download the latest signed update file + * and displaying the status to anyone who asks. + * </p> + * <p>After the download completes the signed update file is verified with + * {@link net.i2p.crypto.TrustedUpdate}, and if it's authentic the payload + * of the signed update file is unpacked and the router is restarted to complete + * the update process. + * </p> */ public class UpdateHandler { private static UpdateRunner _updateRunner; @@ -140,7 +146,7 @@ public class UpdateHandler { public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile) { _status = "<b>Update downloaded</b><br />"; TrustedUpdate up = new TrustedUpdate(_context); - boolean ok = up.migrateVerified(SIGNED_UPDATE_FILE, "i2pupdate.zip"); + boolean ok = up.migrateVerified(RouterVersion.VERSION, SIGNED_UPDATE_FILE, "i2pupdate.zip"); File f = new File(SIGNED_UPDATE_FILE); f.delete(); if (ok) { diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java index c801628a9c..acd4bfc964 100644 --- a/core/java/src/net/i2p/crypto/TrustedUpdate.java +++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java @@ -1,14 +1,15 @@ package net.i2p.crypto; -import java.io.File; -import java.io.FileNotFoundException; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.SequenceInputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.StringTokenizer; +import net.i2p.CoreVersion; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; @@ -18,15 +19,28 @@ import net.i2p.data.SigningPublicKey; import net.i2p.util.Log; /** - * Handles DSA signing and verification of I2P update archives. + * <p>Handles DSA signing and verification of update files. + * </p> + * <p>For convenience this class also makes certain operations available via the + * command line. These can be invoked as follows: + * </p> + * <pre> + * java net.i2p.crypto.TrustedUpdate keygen <i>publicKeyFile privateKeyFile</i> + * java net.i2p.crypto.TrustedUpdate showversion <i>signedFile</i> + * java net.i2p.crypto.TrustedUpdate sign <i>inputFile signedFile privateKeyFile version</i> + * java net.i2p.crypto.TrustedUpdate verifysig <i>signedFile</i> + * java net.i2p.crypto.TrustedUpdate verifyupdate <i>signedFile</i> + * </pre> * - * @author smeghead + * @author jrandom and smeghead */ public class TrustedUpdate { + /** - * default trusted key, generated by jrandom. This can be authenticated - * via gpg without modification (gpg --verify TrustedUpdate.java) - * + * <p>Default trusted key generated by jrandom@i2p.net. This can be + * authenticated via <code>gpg</code> without modification:</p> + * <p> + * <code>gpg --verify TrustedUpdate.java</code></p> */ /* -----BEGIN PGP SIGNED MESSAGE----- @@ -47,334 +61,575 @@ CPah6TDXYJCWmR0n3oPtrvo= -----END PGP SIGNATURE----- */ - private ArrayList _trustedKeys; + private static final String VALID_VERSION_CHARS = "0123456789."; + private static final int VERSION_BYTES = 16; + private static final int HEADER_BYTES = Signature.SIGNATURE_BYTES + VERSION_BYTES; + private static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys"; + + private static I2PAppContext _context; - private I2PAppContext _context; - private Log _log; + private Log _log; + private ArrayList _trustedKeys; - private static final int VERSION_BYTES = 16; - private static final int HEADER_BYTES = VERSION_BYTES + Signature.SIGNATURE_BYTES; - - public static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys"; - + /** + * Constructs a new <code>TrustedUpdate</code> with the default global + * context. + */ public TrustedUpdate() { this(I2PAppContext.getGlobalContext()); } - public TrustedUpdate(I2PAppContext ctx) { - _context = ctx; + + /** + * Constructs a new <code>TrustedUpdate</code> with the given + * {@link net.i2p.I2PAppContext}. + * + * @param context An instance of <code>I2PAppContext</code>. + */ + public TrustedUpdate(I2PAppContext context) { + _context = context; _log = _context.logManager().getLog(TrustedUpdate.class); - _trustedKeys = new ArrayList(1); - String keys = ctx.getProperty(PROP_TRUSTED_KEYS); - if ( (keys != null) && (keys.length() > 0) ) { - StringTokenizer tok = new StringTokenizer(keys, ", "); - while (tok.hasMoreTokens()) - _trustedKeys.add(tok.nextToken()); + _trustedKeys = new ArrayList(); + + String propertyTrustedKeys = context.getProperty(PROP_TRUSTED_KEYS); + + if ( (propertyTrustedKeys != null) && (propertyTrustedKeys.length() > 0) ) { + StringTokenizer propertyTrustedKeysTokens = new StringTokenizer(propertyTrustedKeys, ","); + + while (propertyTrustedKeysTokens.hasMoreTokens()) + _trustedKeys.add(propertyTrustedKeysTokens.nextToken().trim()); + } else { _trustedKeys.add(DEFAULT_TRUSTED_KEY); } } - - public ArrayList getTrustedKeys() { return _trustedKeys; } - - public static void main(String[] args) { - if (args.length <= 0) { - usage(); - } else if ("keygen".equals(args[0])) { - genKeysCLI(args[1], args[2]); - } else if ("sign".equals(args[0])) { - signCLI(args[1], args[2], args[3], args[4]); - } else if ("verify".equals(args[0])) { - verifyCLI(args[1]); - } else { - usage(); + + /** + * Parses command line arguments when this class is used from the command + * line. + * + * @param args Command line parameters. + */ + public static void main(String[] args) { + try { + if ("keygen".equals(args[0])) { + genKeysCLI(args[1], args[2]); + } else if ("showversion".equals(args[0])) { + showVersionCLI(args[1]); + } else if ("sign".equals(args[0])) { + signCLI(args[1], args[2], args[3], args[4]); + } else if ("verifysig".equals(args[0])) { + verifySigCLI(args[1]); + } else if ("verifyupdate".equals(args[0])) { + verifyUpdateCLI(args[1]); + } else { + showUsageCLI(); + } + } catch (ArrayIndexOutOfBoundsException aioobe) { + showUsageCLI(); + } + } + + /** + * Checks if the given version is newer than the given current version. + * + * @param currentVersion The current version. + * @param newVersion The version to test. + * + * @return <code>true</code> if the given version is newer than the current + * version, otherwise <code>false</code>. + */ + public static final boolean needsUpdate(String currentVersion, String newVersion) { + StringTokenizer newVersionTokens = new StringTokenizer(sanitize(newVersion), "."); + StringTokenizer currentVersionTokens = new StringTokenizer(sanitize(currentVersion), "."); + + while (newVersionTokens.hasMoreTokens() && currentVersionTokens.hasMoreTokens()) { + String newNumber = newVersionTokens.nextToken(); + String currentNumber = currentVersionTokens.nextToken(); + + switch (compare(newNumber, currentNumber)) { + case -1: // newNumber is smaller + return false; + case 0: // eq + break; + case 1: // newNumber is larger + return true; + } } - } - private static final void usage() { - System.err.println("Usage: TrustedUpdate keygen publicKeyFile privateKeyFile"); - System.err.println(" TrustedUpdate sign origFile signedFile privateKeyFile version"); - System.err.println(" TrustedUpdate verify signedFile"); + if (newVersionTokens.hasMoreTokens() && !currentVersionTokens.hasMoreTokens()) + return true; + + return false; } - + + private static final int compare(String lop, String rop) { + try { + int left = Integer.parseInt(lop); + int right = Integer.parseInt(rop); + + if (left < right) + return -1; + else if (left == right) + return 0; + else + return 1; + } catch (NumberFormatException nfe) { + return 0; + } + } + private static final void genKeysCLI(String publicKeyFile, String privateKeyFile) { - FileOutputStream out = null; + FileOutputStream fileOutputStream = null; + try { - I2PAppContext ctx = I2PAppContext.getGlobalContext(); - Object keys[] = ctx.keyGenerator().generateSigningKeypair(); - SigningPublicKey pub = (SigningPublicKey)keys[0]; - SigningPrivateKey priv = (SigningPrivateKey)keys[1]; - - out = new FileOutputStream(publicKeyFile); - pub.writeBytes(out); - out.close(); - - out = new FileOutputStream(privateKeyFile); - priv.writeBytes(out); - out.close(); - out = null; - System.out.println("Private keys writen to " + privateKeyFile + " and public to " + publicKeyFile); - System.out.println("Public: " + pub.toBase64()); + Object signingKeypair[] = _context.keyGenerator().generateSigningKeypair(); + SigningPublicKey signingPublicKey = (SigningPublicKey) signingKeypair[0]; + SigningPrivateKey signingPrivateKey = (SigningPrivateKey) signingKeypair[1]; + + fileOutputStream = new FileOutputStream(publicKeyFile); + signingPublicKey.writeBytes(fileOutputStream); + fileOutputStream.close(); + fileOutputStream = null; + + fileOutputStream = new FileOutputStream(privateKeyFile); + signingPrivateKey.writeBytes(fileOutputStream); + + System.out.println("\r\nPrivate key written to: " + privateKeyFile); + System.out.println("Public key written to: " + publicKeyFile); + System.out.println("\r\nPublic key: " + signingPublicKey.toBase64() + "\r\n"); } catch (Exception e) { + System.err.println("Error writing keys:"); e.printStackTrace(); - System.err.println("Error writing out the keys"); } finally { - if (out != null) try { out.close(); } catch (IOException ioe) {} + if (fileOutputStream != null) + try { + fileOutputStream.close(); + } catch (IOException ioe) { + } + } + } + + private static final String sanitize(String versionString) { + StringBuffer versionStringBuffer = new StringBuffer(versionString); + + for (int i = 0; i < versionStringBuffer.length(); i++) { + if (VALID_VERSION_CHARS.indexOf(versionStringBuffer.charAt(i)) == -1) { + versionStringBuffer.deleteCharAt(i); + i--; + } } + + return versionStringBuffer.toString(); } - private static final void signCLI(String origFile, String outFile, String privKeyFile, String version) { - TrustedUpdate up = new TrustedUpdate(); - Signature sig = up.sign(origFile, outFile, privKeyFile, version); - if (sig != null) - System.out.println("Signed and written to " + outFile); + private static final void showUsageCLI() { + System.err.println("Usage: TrustedUpdate keygen publicKeyFile privateKeyFile"); + System.err.println(" TrustedUpdate showversion signedFile"); + System.err.println(" TrustedUpdate sign inputFile signedFile privateKeyFile version"); + System.err.println(" TrustedUpdate verifysig signedFile"); + System.err.println(" TrustedUpdate verifyupdate signedFile"); + } + + private static final void showVersionCLI(String signedFile) { + String versionString = new TrustedUpdate().getVersionString(signedFile); + + if (versionString == "") + System.out.println("No version string found in file '" + signedFile + "'"); else - System.out.println("Error signing"); + System.out.println("Version: " + versionString); } - - private static final void verifyCLI(String signedFile) { - TrustedUpdate up = new TrustedUpdate(); - boolean ok = up.verify(signedFile); - if (ok) + + private static final void signCLI(String inputFile, String signedFile, String privateKeyFile, String version) { + Signature signature = new TrustedUpdate().sign(inputFile, signedFile, privateKeyFile, version); + + if (signature != null) + System.out.println("Input file '" + inputFile + "' signed and written to '" + signedFile + "'"); + else + System.out.println("Error signing input file '" + inputFile + "'"); + } + + private static final void verifySigCLI(String signedFile) { + boolean isValidSignature = new TrustedUpdate().verify(signedFile); + + if (isValidSignature) System.out.println("Signature VALID"); else System.out.println("Signature INVALID"); } - /** - * Reads the version string from a signed I2P update file. - * - * @param inputFile A signed I2P update file. - * - * @return The update version string read, or an empty string if no version - * string is present. - */ - public String getUpdateVersion(String inputFile) { - FileInputStream in = null; + private static final void verifyUpdateCLI(String signedFile) { + boolean isUpdate = new TrustedUpdate().isUpdatedVersion(CoreVersion.VERSION, signedFile); + + if (isUpdate) + System.out.println("File version is newer than current version."); + else + System.out.println("File version is older than or equal to current version."); + } + + /** + * Fetches the trusted keys for the current instance. + * + * @return An <code>ArrayList</code> containting the trusted keys. + */ + public ArrayList getTrustedKeys() { + return _trustedKeys; + } + + /** + * Reads the version string from a signed update file. + * + * @param signedFile A signed update file. + * + * @return The version string read, or an empty string if no version string + * is present. + */ + public String getVersionString(String signedFile) { + FileInputStream fileInputStream = null; + try { - in = new FileInputStream(inputFile); - byte data[] = new byte[VERSION_BYTES]; - int read = DataHelper.read(in, data); - if (read != VERSION_BYTES) - return null; + fileInputStream = new FileInputStream(signedFile); + byte[] data = new byte[VERSION_BYTES]; + int bytesRead = DataHelper.read(fileInputStream, data, Signature.SIGNATURE_BYTES, VERSION_BYTES); + + if (bytesRead != VERSION_BYTES) + return ""; + for (int i = 0; i < VERSION_BYTES; i++) if (data[i] == 0x00) return new String(data, 0, i, "UTF-8"); + return new String(data, "UTF-8"); } catch (UnsupportedEncodingException uee) { - // If this ever gets called, you need a new JVM. throw new RuntimeException("wtf, your JVM doesnt support utf-8? " + uee.getMessage()); - } catch (IOException ioe) { + } catch (IOException ioe) { return ""; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - } - - /** - * Uses the given private key to sign the given input file with DSA. The - * output will be a binary file where the first 16 bytes are the I2P - * update's version string encoded in UTF-8 (padded with trailing - * <code>0h</code> characters if necessary), the next 40 bytes are the - * resulting DSA signature, and the remaining bytes are the input file. - * - * @param inputFile The file to be signed. - * @param outputFile The signed file to write. - * @param privateKeyFile The name of the file containing the private key to - * sign <code>inputFile</code> with. - * @param updateVersion The version number of the I2P update. If this - * string is longer than 16 characters it will be - * truncated. - * - * @return An instance of {@link net.i2p.data.Signature}, or null if there was an error - */ - public Signature sign(String inputFile, String outputFile, String privateKeyFile, String updateVersion) { - SigningPrivateKey key = new SigningPrivateKey(); - FileInputStream in = null; + } + + /** + * Verifies that the version of the given signed update file is newer than + * <code>currentVersion</code>. + * + * @param currentVersion The current version to check against. + * @param signedFile The signed update file. + * + * @return <code>true</code> if the signed update file's version is newer + * than the current version, otherwise <code>false</code>. + */ + public boolean isUpdatedVersion(String currentVersion, String signedFile) { + if (needsUpdate(currentVersion, getVersionString(signedFile))) + return true; + else + return false; + } + + /** + * Verifies the signature of a signed update file, and if it's valid and the + * file's version is newer than the given current version, migrates the data + * out of <code>signedFile</code> and into <code>outputFile</code>. + * + * @param currentVersion The current version to check against. + * @param signedFile A signed update file. + * @param outputFile The file to write the verified data to. + * + * @return <code>true</code> if the signature and version were valid and the + * data was moved, <code>false</code> otherwise. + */ + public boolean migrateVerified(String currentVersion, String signedFile, String outputFile) { + if (!isUpdatedVersion(currentVersion, signedFile)) + return false; + + if (!verify(signedFile)) + return false; + + FileInputStream fileInputStream = null; + FileOutputStream fileOutputStream = null; + try { - in = new FileInputStream(privateKeyFile); - key.readBytes(in); + fileInputStream = new FileInputStream(signedFile); + fileOutputStream = new FileOutputStream(outputFile); + long skipped = 0; + + while (skipped < HEADER_BYTES) + skipped += fileInputStream.skip(HEADER_BYTES - skipped); + + byte[] buffer = new byte[1024]; + int bytesRead = 0; + + while ( (bytesRead = fileInputStream.read(buffer)) != -1) + fileOutputStream.write(buffer, 0, bytesRead); + } catch (IOException ioe) { + return false; + } finally { + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } + + if (fileOutputStream != null) + try { + fileOutputStream.close(); + } catch (IOException ioe) { + } + } + + return true; + } + + /** + * Uses the given private key to sign the given input file along with its + * version string using DSA. The output will be a signed update file where + * the first 40 bytes are the resulting DSA signature, the next 16 bytes are + * the input file's version string encoded in UTF-8 (padded with trailing + * <code>0h</code> characters if necessary), and the remaining bytes are the + * raw bytes of the input file. + * + * @param inputFile The file to be signed. + * @param signedFile The signed update file to write. + * @param privateKeyFile The name of the file containing the private key to + * sign <code>inputFile</code> with. + * @param version The version string of the input file. If this is + * longer than 16 characters it will be truncated. + * + * @return An instance of {@link net.i2p.data.Signature}, or + * <code>null</code> if there was an error. + */ + public Signature sign(String inputFile, String signedFile, String privateKeyFile, String version) { + FileInputStream fileInputStream = null; + SigningPrivateKey signingPrivateKey = new SigningPrivateKey(); + + try { + fileInputStream = new FileInputStream(privateKeyFile); + signingPrivateKey.readBytes(fileInputStream); } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signing key", ioe); + return null; } catch (DataFormatException dfe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signing key", dfe); + return null; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - - return sign(inputFile, outputFile, key, updateVersion); + + return sign(inputFile, signedFile, signingPrivateKey, version); } - - public Signature sign(String inputFile, String outputFile, SigningPrivateKey privKey, String updateVersion) { - byte[] headerUpdateVersion = { - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 }; - byte[] updateVersionBytes = null; - if (updateVersion.length() > VERSION_BYTES) - updateVersion = updateVersion.substring(0, VERSION_BYTES); - try { - updateVersionBytes = updateVersion.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - // If this ever gets called, you need a new JVM. + + /** + * Uses the given {@link net.i2p.data.SigningPrivateKey} to sign the given + * input file along with its version string using DSA. The output will be a + * signed update file where the first 40 bytes are the resulting DSA + * signature, the next 16 bytes are the input file's version string encoded + * in UTF-8 (padded with trailing <code>0h</code> characters if necessary), + * and the remaining bytes are the raw bytes of the input file. + * + * @param inputFile The file to be signed. + * @param signedFile The signed update file to write. + * @param signingPrivateKey An instance of <code>SigningPrivateKey</code> + * to sign <code>inputFile</code> with. + * @param version The version string of the input file. If this is + * longer than 16 characters it will be truncated. + * + * @return An instance of {@link net.i2p.data.Signature}, or + * <code>null</code> if there was an error. + */ + public Signature sign(String inputFile, String signedFile, SigningPrivateKey signingPrivateKey, String version) { + byte[] versionHeader = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + byte[] versionRawBytes = null; + + if (version.length() > VERSION_BYTES) + version = version.substring(0, VERSION_BYTES); + + try { + versionRawBytes = version.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { throw new RuntimeException("wtf, your JVM doesnt support utf-8? " + e.getMessage()); - } - System.arraycopy(updateVersionBytes, 0, headerUpdateVersion, 0, updateVersionBytes.length); + } + System.arraycopy(versionRawBytes, 0, versionHeader, 0, versionRawBytes.length); + + FileInputStream fileInputStream = null; Signature signature = null; - FileInputStream in = null; + SequenceInputStream bytesToSignInputStream = null; + ByteArrayInputStream versionHeaderInputStream = null; + try { - in = new FileInputStream(inputFile); - signature = _context.dsa().sign(in, privKey); + fileInputStream = new FileInputStream(inputFile); + versionHeaderInputStream = new ByteArrayInputStream(versionHeader); + bytesToSignInputStream = new SequenceInputStream(versionHeaderInputStream, fileInputStream); + signature = _context.dsa().sign(bytesToSignInputStream, signingPrivateKey); + } catch (Exception e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error signing", e); + return null; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - in = null; + if (bytesToSignInputStream != null) + try { + bytesToSignInputStream.close(); + } catch (IOException ioe) { + } + + fileInputStream = null; } + FileOutputStream fileOutputStream = null; + try { - fileOutputStream = new FileOutputStream(outputFile); - fileOutputStream.write(headerUpdateVersion); - fileOutputStream.write(signature.getData()); - - in = new FileInputStream(inputFile); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - fileOutputStream.write(buf, 0, read); - fileOutputStream.close(); - fileOutputStream = null; - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.log(Log.WARN, "Error writing signed I2P update file " + outputFile, ioe); + fileOutputStream = new FileOutputStream(signedFile); + fileOutputStream.write(signature.getData()); + fileOutputStream.write(versionHeader); + fileInputStream = new FileInputStream(inputFile); + byte[] buffer = new byte[1024]; + int bytesRead = 0; + while ( (bytesRead = fileInputStream.read(buffer)) != -1) + fileOutputStream.write(buffer, 0, bytesRead); + fileOutputStream.close(); + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.log(Log.WARN, "Error writing signed file " + signedFile, ioe); + return null; - } finally { - if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ioe) {} - if (in != null) try { in.close(); } catch (IOException ioe) {} + } finally { + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } + + if (fileOutputStream != null) + try { + fileOutputStream.close(); + } catch (IOException ioe) { + } } - return signature; - } - - /** - * Verifies the DSA signature of a signed I2P update. - * - * @param inputFile The signed update file to check. - * - * @return <code>true</code> if the file has a valid signature. - */ - public boolean verify(String inputFile) { + return signature; + } + + /** + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * + * @return <code>true</code> if the file has a valid signature, otherwise + * <code>false</code>. + */ + public boolean verify(String signedFile) { for (int i = 0; i < _trustedKeys.size(); i++) { - SigningPublicKey key = new SigningPublicKey(); + SigningPublicKey signingPublicKey = new SigningPublicKey(); + try { - key.fromBase64((String)_trustedKeys.get(i)); - boolean ok = verify(inputFile, key); - if (ok) return true; + signingPublicKey.fromBase64((String)_trustedKeys.get(i)); + boolean isValidSignature = verify(signedFile, signingPublicKey); + + if (isValidSignature) + return true; } catch (DataFormatException dfe) { _log.log(Log.CRIT, "Trusted key " + i + " is not valid"); } } + if (_log.shouldLog(Log.WARN)) _log.warn("None of the keys match"); + return false; } - - /** - * Verifies the DSA signature of a signed I2P update. - * - * @param inputFile The signed update file to check. - * @param key public key to verify against - * - * @return <code>true</code> if the file has a valid signature. - */ - public boolean verify(String inputFile, SigningPublicKey key) { - FileInputStream in = null; - try { - in = new FileInputStream(inputFile); - byte version[] = new byte[VERSION_BYTES]; - Signature sig = new Signature(); - if (VERSION_BYTES != DataHelper.read(in, version)) - throw new IOException("Not enough data for the version bytes"); - sig.readBytes(in); - return _context.dsa().verifySignature(sig, in, key); - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error reading " + inputFile + " to verify", ioe); - return false; - } catch (DataFormatException dfe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error reading the signature", dfe); - return false; - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - } - } - - /** - * Verifies the DSA signature of a signed I2P update. - * - * @param inputFile The signed update file to check. - * @param publicKeyFile The public key to use for verification. - * - * @return <code>true</code> if the file has a valid signature. - */ - public boolean verify(String inputFile, String publicKeyFile) { - SigningPublicKey pub = new SigningPublicKey(); - FileInputStream in = null; + + /** + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * @param publicKeyFile A file containing the public key to use for + * verification. + * + * @return <code>true</code> if the file has a valid signature, otherwise + * <code>false</code>. + */ + public boolean verify(String signedFile, String publicKeyFile) { + SigningPublicKey signingPublicKey = new SigningPublicKey(); + FileInputStream fileInputStream = null; + try { - in = new FileInputStream(inputFile); - pub.readBytes(in); + fileInputStream = new FileInputStream(signedFile); + signingPublicKey.readBytes(fileInputStream); } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signature", ioe); + return false; } catch (DataFormatException dfe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signature", dfe); + return false; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - - return verify(inputFile, pub); - } - + + return verify(signedFile, signingPublicKey); + } + /** - * Verify the signature on the signed inputFile, and if it is valid, migrate - * the raw data out of it and into the outputFile - * - * @return true if the signature was valid and the data moved, false otherwise. + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * @param signingPublicKey An instance of + * {@link net.i2p.data.SigningPublicKey} to use for + * verification. + * + * @return <code>true</code> if the file has a valid signature, otherwise + * <code>false</code>. */ - public boolean migrateVerified(String inputFile, String outputFile) { - boolean ok = verify(inputFile); - if (!ok) return false; - FileOutputStream out = null; - FileInputStream in = null; + public boolean verify(String signedFile, SigningPublicKey signingPublicKey) { + FileInputStream fileInputStream = null; + try { - out = new FileOutputStream(outputFile); - in = new FileInputStream(inputFile); - long skipped = 0; - while (skipped < HEADER_BYTES) { - skipped += in.skip(HEADER_BYTES - skipped); - } - - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); + fileInputStream = new FileInputStream(signedFile); + Signature signature = new Signature(); + + signature.readBytes(fileInputStream); + + return _context.dsa().verifySignature(signature, fileInputStream, signingPublicKey); } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error reading " + signedFile + " to verify", ioe); + + return false; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error reading the signature", dfe); + return false; } finally { - if (out != null) try { out.close(); } catch (IOException ioe) {} - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - return true; } } diff --git a/history.txt b/history.txt index 91ac6030a6..3488ff7069 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,14 @@ -$Id: history.txt,v 1.188 2005/04/05 17:24:32 jrandom Exp $ +$Id: history.txt,v 1.189 2005/04/06 10:43:25 jrandom Exp $ + +2005-04-08 smeghead + * Security improvements to TrustedUpdate: signing and verification of the + version string along with the data payload for signed update files + (consequently the positions of the DSA signature and version string fields + have been swapped in the spec for the update file's header); router will + no longer perform a trusted update if the signed update's version is lower + than or equal to the currently running router's version. + * Added two new CLI commands to TrustedUpdate: showversion, verifyupdate. + * Extended TrustedUpdate public API for use by third party applications. * 2005-04-06 0.5.0.6 released -- GitLab