forked from I2P_Developers/i2p.i2p
* Updates:
- Add the router version to the zip file comment in the updater - Add a class to extract the zip file comment - Require the sud version header to match the zip file comment to prevent spoofing of the version number, since the version number in the header is not covered by the sud signature.
This commit is contained in:
@ -22,6 +22,7 @@ import net.i2p.data.SigningPrivateKey;
|
||||
import net.i2p.data.SigningPublicKey;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.VersionComparator;
|
||||
import net.i2p.util.ZipFileComment;
|
||||
|
||||
/**
|
||||
* <p>Handles DSA signing and verification of update files.
|
||||
@ -35,6 +36,7 @@ import net.i2p.util.VersionComparator;
|
||||
* 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
|
||||
@ -242,6 +244,8 @@ JXQAnA28vDmMMMH/WPbC5ixmJeGGNUiR
|
||||
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();
|
||||
}
|
||||
@ -301,11 +305,12 @@ JXQAnA28vDmMMMH/WPbC5ixmJeGGNUiR
|
||||
}
|
||||
|
||||
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("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 */
|
||||
@ -349,9 +354,29 @@ JXQAnA28vDmMMMH/WPbC5ixmJeGGNUiR
|
||||
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.
|
||||
*
|
||||
@ -490,6 +515,13 @@ JXQAnA28vDmMMMH/WPbC5ixmJeGGNUiR
|
||||
* 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.
|
||||
@ -507,12 +539,32 @@ JXQAnA28vDmMMMH/WPbC5ixmJeGGNUiR
|
||||
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
|
||||
*/
|
||||
private boolean verifyVersionMatch(File signedFile) {
|
||||
try {
|
||||
String zipComment = 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.
|
||||
*
|
||||
|
111
core/java/src/net/i2p/util/ZipFileComment.java
Normal file
111
core/java/src/net/i2p/util/ZipFileComment.java
Normal file
@ -0,0 +1,111 @@
|
||||
package net.i2p.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
/**
|
||||
* Not available in ZipFile until Java 7. Refs:
|
||||
* https://secure.wikimedia.org/wikipedia/en/wiki/ZIP_%28file_format%29
|
||||
* http://download.oracle.com/javase/1.5.0/docs/api/java/util/zip/ZipFile.html
|
||||
* http://bugs.sun.com/view_bug.do?bug_id=6646605
|
||||
*
|
||||
* Code modified from:
|
||||
* http://www.flattermann.net/2009/01/read-a-zip-file-comment-with-java/
|
||||
* Beerware.
|
||||
*
|
||||
* since 0.8.8
|
||||
*/
|
||||
public abstract class ZipFileComment {
|
||||
|
||||
private static final int BLOCK_LEN = 22;
|
||||
private static final byte[] magicStart = {0x50, 0x4b, 0x03, 0x04};
|
||||
private static final int HEADER_LEN = magicStart.length;
|
||||
private static final byte[] magicDirEnd = {0x50, 0x4b, 0x05, 0x06};
|
||||
private static final int MAGIC_LEN = magicDirEnd.length;
|
||||
|
||||
/**
|
||||
* @param max The max length of the comment in bytes.
|
||||
* If the actual comment is longer, it will not be found and
|
||||
* this method will throw an IOE
|
||||
*
|
||||
* @return empty string if no comment, or the comment.
|
||||
* The string is decoded with UTF-8
|
||||
*
|
||||
* @throws IOE if no valid end-of-central-directory record found
|
||||
*/
|
||||
public static String getComment(File file, int max) throws IOException {
|
||||
return getComment(file, max, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param max The max length of the comment in bytes.
|
||||
* If the actual comment is longer, it will not be found and
|
||||
* this method will throw an IOE
|
||||
* @param skip Number of bytes to skip in the file before looking for the
|
||||
* zip header. Use 56 for sud/su2 files.
|
||||
*
|
||||
* @return empty string if no comment, or the comment.
|
||||
* The string is decoded with UTF-8
|
||||
*
|
||||
* @throws IOE if no valid end-of-central-directory record found
|
||||
*/
|
||||
public static String getComment(File file, int max, int skip) throws IOException {
|
||||
if (!file.exists())
|
||||
throw new FileNotFoundException("File not found: " + file);
|
||||
long len = file.length();
|
||||
if (len < BLOCK_LEN + HEADER_LEN + skip)
|
||||
throw new ZipException("File too short: " + file);
|
||||
if (len > Integer.MAX_VALUE)
|
||||
throw new ZipException("File too long: " + file);
|
||||
int fileLen = (int) len;
|
||||
byte[] buffer = new byte[Math.min(fileLen - skip, max + BLOCK_LEN)];
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(file);
|
||||
if (skip > 0)
|
||||
in.skip(skip);
|
||||
byte[] hdr = new byte[HEADER_LEN];
|
||||
DataHelper.read(in, hdr);
|
||||
if (!DataHelper.eq(hdr, magicStart))
|
||||
throw new ZipException("Not a zip file: " + file);
|
||||
in.skip(fileLen - (skip + HEADER_LEN + buffer.length));
|
||||
DataHelper.read(in, buffer);
|
||||
return getComment(buffer);
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* go backwards from the end
|
||||
*/
|
||||
private static String getComment(byte[] buffer) throws IOException {
|
||||
for (int i = buffer.length - (1 + BLOCK_LEN - MAGIC_LEN); i >= 0; i--) {
|
||||
if (DataHelper.eq(buffer, i, magicDirEnd, 0, MAGIC_LEN)) {
|
||||
int commentLen = (buffer[i + BLOCK_LEN - 2] & 0xff) +
|
||||
((buffer[i + BLOCK_LEN - 1] & 0xff) * 256);
|
||||
return new String(buffer, i + BLOCK_LEN, commentLen, "UTF-8");
|
||||
}
|
||||
}
|
||||
throw new ZipException("No comment block found");
|
||||
}
|
||||
|
||||
public static void main(String args[]) throws IOException {
|
||||
if (args.length != 1) {
|
||||
System.err.println("Usage: ZipFileComment file");
|
||||
return;
|
||||
}
|
||||
int skip = 0;
|
||||
String file = args[0];
|
||||
if (file.endsWith(".sud") || file.endsWith(".su2"))
|
||||
skip = 56;
|
||||
String c = getComment(new File(file), 256, skip);
|
||||
System.out.println("comment is: \"" + c + '\"');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user