package net.i2p.crypto; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.IOException; import java.io.SequenceInputStream; import java.io.UnsupportedEncodingException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import net.i2p.CoreVersion; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Signature; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; import net.i2p.util.Log; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.VersionComparator; //import net.i2p.util.ZipFileComment; /** * <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> * java net.i2p.crypto.TrustedUpdate verifyversion <i>signedFile</i> * </pre> * * @author jrandom and smeghead */ public class TrustedUpdate { /** * <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----- Hash: SHA1 */ private static final String DEFAULT_TRUSTED_KEY = "W4kJbnv9KSVwbnapV7SaNW2kMIZKs~hwL0ro9pZXFo1xTwqz45nykCp1H" + "M7sAKYDZay5z1HvYYOl9CNVz00xF03KPU9RUCVxhDZ1YXhZIskPKjUPUs" + "CIpE~Z1C~N9KSEV6~2stDlBNH10VZ4T0X1TrcXwb3IBXliWo2y2GAx~Ow="; /* -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.2.4 (GNU/Linux) iD8DBQFCZ38IWYfZ3rPnHH0RAgOHAJ4wNgmfO2AkL8IXiGnPtWrTlXcVogCfQ79z jP69nPbh4KLGhF+SD0+0bW4= =npPe -----END PGP SIGNATURE----- */ /* -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 */ /* zzz's key */ private static final String DEFAULT_TRUSTED_KEY2 = "lT54eq3SH0TWWwQ1wgH6XPelIno7wH7UfiZOpQg-ZuxdNhc4UjjrohKdK" + "Zqfswt1ANPnmOlMewLGBESl7kJB9c5sByz~IOlNyz5BMLRC~R~ZC9QI4W" + "XwUBYW8BhYO2mkvtdOrcy690lDkwzdf5xLxlCBpQlTaLYzQVjVWBcvbCA="; /* -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.6 (GNU/Linux) iD8DBQFHdupcQVV2uqduC+0RAocuAKCR4ILLuz3RB8QT7zkadmS2LmFuMwCgweqG lFm5Fqx/iW5+k0QaQZ3W9mY= =V3i7 -----END PGP SIGNATURE----- */ /* -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 */ /* Complication's key */ private static final String DEFAULT_TRUSTED_KEY3 = "JHFA0yXUgKtmhajXFZH9Nk62OPRHbvvQHTi8EANV-D~3tjLjaz9p9cs6F" + "s8W3FSLfUwsQeFg7dfVSQQZga~1jMjboo94vIcm3j6XbW4mbcorVQ74uP" + "jd8EA1AQhJ6bBTxDAFk~6fVDOdhHT0Wo5CcUn7v8bAYY3x3UWiL8Remx0="; /* -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.6 (GNU/Linux) iD8DBQFHphOV+h38a3n8zjMRAll+AJ9KA6WiDJcTN4qfrslSemUMr+FBrwCeM8pF D8usM7Dxp5yrDrCYZ5AIijc= =SrXI -----END PGP SIGNATURE----- */ /* -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 */ /* HungryHobo's key */ private static final String DEFAULT_TRUSTED_KEY4 = "l3G6um9nB9EDLkT9cUusz5fX-GxXSWE5zaj2~V8lUL~XsGuFf8gKqzJLK" + "NkAw0CgDIDsLRHHuUaF7ZHo5Z7HG~9JJU9Il4G2jyNYtg5S8AzG0UxkEt" + "-JeBEqIxv5GDn6OFKr~wTI0UafJbegEWokl-8m-GPWf0vW-yPMjL7y5MI="; /* -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) iEYEARECAAYFAkysnNIACgkQHix7YXbc3BJVfwCeNGUHaWSqZUbWN9L8VyQLpwxI JXQAnA28vDmMMMH/WPbC5ixmJeGGNUiR =3oMC -----END PGP SIGNATURE----- */ /* -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 */ /* KillYourTV's key */ private static final String DEFAULT_TRUSTED_KEY5 = "DAVvT6zMcRuzJi3V8DKKV6o0GjXoQsEwnJsFMaVG1Se-KPQjfP8PbgKJD" + "crFe0zNJfh3yPdsocA~A~s9U6pvimlCXH2pnJGlNNojtFCZC3DleROl5-" + "4EkYw~UKAg940o5yg1OCBVlRZBSrRAQIIjFGkxxPQc12dA~cfpryNk7Dc="; /* -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.11 (GNU/Linux) iQEcBAEBAgAGBQJO7TSnAAoJEKvgwxnfCgoaJVIIAJbJNdwgqCHkmgPeBEWZbtaM EkmIL4UC75wVD8yiYReKreX7tJCL7NaeJvnNMNItgy4qJpr+bY0TkJ/LcFoq9ugE ABBRJD2XDPFjPWYQ0nTiFj3IpWdbxLZAAXXFttyFLDdw52aWUH7nd6TdxFHh1Ssi pU0yyu77FP5iq3dSTPZUEpA8NB/T6ImbqKQqRltst+TdnbzEwwFB23cihA286cJX rcoh8CyklYiT3wr46epmHEetseEffxktvn+iCbtRpkA0oLXdVQ0d8cNuB00YUEyB riCe6OlAEiNpcc6mMyIYYWFICbrDFTrDR3wXqwc/Jkcx6L5VVWoagpSzbo3yGhc= =8ix/ -----END PGP SIGNATURE----- */ private static final int VERSION_BYTES = 16; public static final int HEADER_BYTES = Signature.SIGNATURE_BYTES + VERSION_BYTES; private static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys"; private final I2PAppContext _context; private final Log _log; private final Map<SigningPublicKey, String> _trustedKeys; private String _newVersion; /** 172 */ private static final int KEYSIZE_B64_BYTES = 2 + (SigningPublicKey.KEYSIZE_BYTES * 4 / 3); private static final Map<String, String> DEFAULT_KEYS = new HashMap<String, String>(4); static { //DEFAULT_KEYS.put(DEFAULT_TRUSTED_KEY, "jrandom@mail.i2p"); DEFAULT_KEYS.put(DEFAULT_TRUSTED_KEY2, "zzz@mail.i2p"); //DEFAULT_KEYS.put(DEFAULT_TRUSTED_KEY3, "complication@mail.i2p"); DEFAULT_KEYS.put(DEFAULT_TRUSTED_KEY4, "HungryHobo@mail.i2p"); DEFAULT_KEYS.put(DEFAULT_TRUSTED_KEY5, "killyourtv@mail.i2p"); } /** * Constructs a new <code>TrustedUpdate</code> with the default global * context. */ public TrustedUpdate() { this(I2PAppContext.getGlobalContext()); } /** * 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 HashMap<SigningPublicKey, String>(4); String propertyTrustedKeys = context.getProperty(PROP_TRUSTED_KEYS); if ( (propertyTrustedKeys != null) && (propertyTrustedKeys.length() > 0) ) { StringTokenizer propertyTrustedKeysTokens = new StringTokenizer(propertyTrustedKeys, " ,\r\n"); while (propertyTrustedKeysTokens.hasMoreTokens()) { // If a key from the defaults, use the same name String key = propertyTrustedKeysTokens.nextToken().trim(); String name = DEFAULT_KEYS.get(key); if (name == null) name = ""; addKey(key, name); } } else { for (Map.Entry<String, String> e : DEFAULT_KEYS.entrySet()) { addKey(e.getKey(), e.getValue()); } } if (_log.shouldLog(Log.DEBUG)) _log.debug("TrustedUpdate created, trusting " + _trustedKeys.size() + " keys."); } /** * @since 0.9.8, public since 0.9.14.1 */ public Map<SigningPublicKey, String> getKeys() { return Collections.unmodifiableMap(_trustedKeys); } /** * Duplicate keys or names rejected, * except that duplicate empty names are allowed * @param key 172 character base64 string * @param name non-null but "" ok * @since 0.7.12 * @return true if successful */ public boolean addKey(String key, String name) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Adding " + name + ": " + key); SigningPublicKey signingPublicKey = new SigningPublicKey(); try { // fromBase64() will throw a DFE if length is not right signingPublicKey.fromBase64(key); } catch (DataFormatException dfe) { _log.error("Invalid signing key for " + name + " : " + key, dfe); return false; } String oldName = _trustedKeys.get(signingPublicKey); // already there? if (name.equals(oldName)) return true; if (oldName != null && !oldName.equals("")) { _log.error("Key for " + name + " already stored for different name " + oldName + " : " + key); return false; } if ((!name.equals("")) && _trustedKeys.containsValue(name)) { _log.error("Key mismatch for " + name + ", spoof attempt? : " + key); return false; } _trustedKeys.put(signingPublicKey, name); return true; } /** * Do we know about the following key? * @since 0.7.12 */ public boolean haveKey(String key) { if (key.length() != KEYSIZE_B64_BYTES) return false; SigningPublicKey signingPublicKey = new SigningPublicKey(); try { signingPublicKey.fromBase64(key); } catch (DataFormatException dfe) { return false; } return _trustedKeys.containsKey(signingPublicKey); } /** * Parses command line arguments when this class is used from the command * line. * Exits 1 on failure so this can be used in scripts. * * @param args Command line parameters. */ public static void main(String[] args) { boolean ok = false; try { if ("keygen".equals(args[0])) { ok = genKeysCLI(args[1], args[2]); } else if ("showversion".equals(args[0])) { ok = showVersionCLI(args[1]); } else if ("sign".equals(args[0])) { ok = signCLI(args[1], args[2], args[3], args[4]); } else if ("verifysig".equals(args[0])) { ok = verifySigCLI(args[1]); } else if ("verifyupdate".equals(args[0])) { ok = verifyUpdateCLI(args[1]); } else if ("verifyversion".equals(args[0])) { ok = verifyVersionCLI(args[1]); } else { showUsageCLI(); } } catch (ArrayIndexOutOfBoundsException aioobe) { showUsageCLI(); } if (!ok) System.exit(1); } /** * 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) { return VersionComparator.comp(currentVersion, newVersion) < 0; } /** @return success */ private static final boolean genKeysCLI(String publicKeyFile, String privateKeyFile) { File pubFile = new File(publicKeyFile); File privFile = new File(privateKeyFile); if (pubFile.exists()) { System.out.println("Error: Not overwriting file " + publicKeyFile); return false; } if (privFile.exists()) { System.out.println("Error: Not overwriting file " + privateKeyFile); return false; } FileOutputStream fileOutputStream = null; I2PAppContext context = I2PAppContext.getGlobalContext(); try { Object signingKeypair[] = context.keyGenerator().generateSigningKeypair(); SigningPublicKey signingPublicKey = (SigningPublicKey) signingKeypair[0]; SigningPrivateKey signingPrivateKey = (SigningPrivateKey) signingKeypair[1]; fileOutputStream = new SecureFileOutputStream(pubFile); signingPublicKey.writeBytes(fileOutputStream); fileOutputStream.close(); fileOutputStream = null; fileOutputStream = new SecureFileOutputStream(privFile); 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 (IOException e) { System.err.println("Error writing keys:"); e.printStackTrace(); return false; } catch (DataFormatException e) { System.err.println("Error writing keys:"); e.printStackTrace(); return false; } finally { if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ioe) { } } return true; } 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"); System.err.println(" TrustedUpdate verifyversion signedFile"); } /** @return success */ private static final boolean showVersionCLI(String signedFile) { String versionString = getVersionString(new File(signedFile)); if (versionString.equals("")) System.out.println("No version string found in file '" + signedFile + "'"); else System.out.println("Version: " + versionString); return !versionString.equals(""); } /** @return success */ private static final boolean 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 + "'"); return signature != null; } /** @return valid */ private static final boolean verifySigCLI(String signedFile) { boolean isValidSignature = new TrustedUpdate().verify(new File(signedFile)); if (isValidSignature) System.out.println("Signature VALID"); else System.out.println("Signature INVALID"); return isValidSignature; } /** @return if newer */ private static final boolean verifyUpdateCLI(String signedFile) { boolean isUpdate = new TrustedUpdate().isUpdatedVersion(CoreVersion.VERSION, new File(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."); return isUpdate; } /** * @return true if there's no version mismatch * @since 0.8.8 */ private static final boolean verifyVersionCLI(String signedFile) { TrustedUpdate tu = new TrustedUpdate(); File file = new File(signedFile); // ignore result, just used to read in version tu.isUpdatedVersion("0", file); boolean isMatch = tu.verifyVersionMatch(file); if (isMatch) System.out.println("Version verified"); else System.out.println("Version mismatch, header version does not match zip comment version"); return isMatch; } /** * Fetches the trusted keys for the current instance. * * @return An <code>ArrayList</code> containting the trusted keys. */ /*** public ArrayList getTrustedKeys() { return _trustedKeys; } ***/ /** * Fetches the trusted keys for the current instance. * We could sort it but don't bother. * * @return A <code>String</code> containing the trusted keys, * delimited by CR LF line breaks. */ public String getTrustedKeysString() { StringBuilder buf = new StringBuilder(1024); for (SigningPublicKey spk : _trustedKeys.keySet()) { // If something already buffered, first add line break. if (buf.length() > 0) buf.append("\r\n"); buf.append(spk.toBase64()); } return buf.toString(); } /** * 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 static String getVersionString(File signedFile) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(signedFile); DataHelper.skip(fileInputStream, Signature.SIGNATURE_BYTES); byte[] data = new byte[VERSION_BYTES]; int bytesRead = DataHelper.read(fileInputStream, data); 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) { throw new RuntimeException("your JVM doesnt support utf-8? " + uee.getMessage()); } catch (IOException ioe) { return ""; } finally { if (fileInputStream != null) try { fileInputStream.close(); } catch (IOException ioe) { } } } /** * Reads the version string from an input stream * * @param inputStream containing at least 56 bytes * * @return The version string read, or an empty string if no version string * is present. * * @since 0.7.12 */ public static String getVersionString(InputStream inputStream) { try { DataHelper.skip(inputStream, Signature.SIGNATURE_BYTES); byte[] data = new byte[VERSION_BYTES]; int bytesRead = DataHelper.read(inputStream, data); 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) { throw new RuntimeException("your JVM doesnt support utf-8? " + uee.getMessage()); } catch (IOException ioe) { return ""; } finally { if (inputStream != null) try { inputStream.close(); } catch (IOException ioe) { } } } /** version in the .sud file, valid only after calling migrateVerified() */ public String newVersion() { return _newVersion; } /** * 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, File signedFile) { _newVersion = getVersionString(signedFile); return needsUpdate(currentVersion, _newVersion); } /** * 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>. * * As of 0.8.8, the embedded file must be a zip file with * a standard zip header and a UTF-8 zip file comment * matching the version in the sud header. This prevents spoofing the version, * since the sud signature does NOT cover the version in the header. * (We do this for sud/su2 files but not plugin xpi2p files - * don't use this method for plugin files) * * @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>null</code> if the signature and version were valid and the * data was moved, and an error <code>String</code> otherwise. */ public String migrateVerified(String currentVersion, File signedFile, File outputFile) { if (!signedFile.exists()) return "File not found: " + signedFile.getAbsolutePath(); if (!isUpdatedVersion(currentVersion, signedFile)) { if ("".equals(_newVersion)) return "Truncated or corrupt file: " + signedFile.getAbsolutePath(); else return "Downloaded version is not greater than current version"; } if (!verifyVersionMatch(signedFile)) return "Update file invalid - signed version mismatch"; if (!verify(signedFile)) return "Unknown signing key or corrupt file"; return migrateFile(signedFile, outputFile); } /** * Verify the version in the sud header matches the version in the zip comment * (and that the embedded file is a zip file at all) * isUpdatedVersion() must be called first to set _newVersion. * * @return true if matches * * @since 0.8.8 */ @SuppressWarnings("deprecation") private boolean verifyVersionMatch(File signedFile) { try { String zipComment = net.i2p.util.ZipFileComment.getComment(signedFile, VERSION_BYTES, HEADER_BYTES); return zipComment.equals(_newVersion); } catch (IOException ioe) {} return false; } /** * Extract the file. Skips and ignores the signature and version. No verification. * * @param signedFile A signed update file. * @param outputFile The file to write the verified data to. * * @return <code>null</code> if the * data was moved, and an error <code>String</code> otherwise. * * @since 0.7.12 */ public String migrateFile(File signedFile, File outputFile) { if (!signedFile.exists()) return "File not found: " + signedFile.getAbsolutePath(); FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null; try { fileInputStream = new FileInputStream(signedFile); fileOutputStream = new FileOutputStream(outputFile); DataHelper.skip(fileInputStream, HEADER_BYTES); DataHelper.copy(fileInputStream, fileOutputStream); } catch (IOException ioe) { // probably permissions or disk full, so bring the message out to the console return "Error copying update: " + ioe; } finally { if (fileInputStream != null) try { fileInputStream.close(); } catch (IOException ioe) { } if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ioe) { } } return null; } /** * 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 (fileInputStream != null) try { fileInputStream.close(); } catch (IOException ioe) { } } return sign(inputFile, signedFile, signingPrivateKey, version); } /** * 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("your JVM doesnt support utf-8? " + e.getMessage()); } System.arraycopy(versionRawBytes, 0, versionHeader, 0, versionRawBytes.length); FileInputStream fileInputStream = null; Signature signature = null; SequenceInputStream bytesToSignInputStream = null; ByteArrayInputStream versionHeaderInputStream = null; try { fileInputStream = new FileInputStream(inputFile); versionHeaderInputStream = new ByteArrayInputStream(versionHeader); bytesToSignInputStream = new SequenceInputStream(versionHeaderInputStream, fileInputStream); signature = _context.dsa().sign(bytesToSignInputStream, signingPrivateKey); } catch (IOException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error signing", e); return null; } finally { if (bytesToSignInputStream != null) try { bytesToSignInputStream.close(); fileInputStream.close(); } catch (IOException ioe) { } } FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(signedFile); fileOutputStream.write(signature.getData()); fileOutputStream.write(versionHeader); fileInputStream = new FileInputStream(inputFile); DataHelper.copy(fileInputStream, fileOutputStream); fileOutputStream.close(); } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.log(Log.WARN, "Error writing signed file " + signedFile, ioe); return null; } 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 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(File signedFile) { for (SigningPublicKey signingPublicKey : _trustedKeys.keySet()) { boolean isValidSignature = verify(signedFile, signingPublicKey); if (isValidSignature) return true; } if (_log.shouldLog(Log.WARN)) _log.warn("None of the keys match"); return false; } /** * Verifies the DSA signature of a signed update file. * * @param signedFile The signed update file to check. * * @return signer (could be empty string) or null if invalid * @since 0.7.12 */ public String verifyAndGetSigner(File signedFile) { for (SigningPublicKey signingPublicKey : _trustedKeys.keySet()) { boolean isValidSignature = verify(signedFile, signingPublicKey); if (isValidSignature) return _trustedKeys.get(signingPublicKey); } return 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 { 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 (fileInputStream != null) try { fileInputStream.close(); } catch (IOException ioe) { } } return verify(new File(signedFile), signingPublicKey); } /** * 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 verify(File signedFile, SigningPublicKey signingPublicKey) { FileInputStream fileInputStream = null; try { 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 (fileInputStream != null) try { fileInputStream.close(); } catch (IOException ioe) { } } } }