// --- BEGIN COPYRIGHT BLOCK --- // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; version 2 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // // (C) 2007 Red Hat, Inc. // All rights reserved. // --- END COPYRIGHT BLOCK --- package com.netscape.cmstools; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; import java.util.List; import java.util.StringTokenizer; import java.util.Vector; import org.mozilla.jss.CryptoManager; import org.mozilla.jss.crypto.ObjectNotFoundException; import org.mozilla.jss.crypto.X509Certificate; import com.netscape.cmsutil.util.Utils; import netscape.security.x509.X509CertImpl; /** * Tool for verifying signed audit logs * * @version $Revision$, $Date$ */ public class AuditVerify { public static final String CRYPTO_PROVIDER = "Mozilla-JSS"; // We always sign 0x0a as the line separator, regardless of what // line separator characters are used in the log file. This helps // signature verification be platform-independent. private static final byte LINE_SEP_BYTE = 0x0a; boolean verbose; X509Certificate signingCert; public AuditVerify() { } public void setVerbose(boolean verbose) { this.verbose = verbose; } public void setSigningCert(X509Certificate signingCert) throws Exception { // verify audit signing certificate // not checking validity because we want to allow verifying old logs if (signingCert == null) { throw new Exception("Missing signing certificate"); } byte[] bytes = signingCert.getEncoded(); X509CertImpl cert = new X509CertImpl(bytes); boolean[] keyUsage = cert.getKeyUsage(); if (keyUsage == null || keyUsage.length < 1) { throw new Exception("Missing signing certificate key usage"); } boolean valid = keyUsage[0]; if (!valid) { throw new Exception("Invalid signing certificate key usage"); } this.signingCert = signingCert; } private static void usage() { System.out .println("Usage: AuditVerify -d -n -a [-P ] [-v]"); System.exit(1); } public static byte[] base64decode(String input) throws Exception { return Utils.base64decode(input); } private static void output(int linenum, String mesg) throws IOException { System.out.println("Line " + linenum + ": " + mesg); } private static void writeFile(String curfileName) { System.out.println("======\nFile: " + curfileName + "\n======"); } private static void writeSigStatus(int linenum, String sigStartFile, int sigStartLine, String sigStopFile, int sigStopLine, String mesg) throws IOException { output(linenum, mesg + ": signature of " + sigStartFile + ":" + sigStartLine + " to " + sigStopFile + ":" + sigStopLine); } private static class PrefixFilter implements FilenameFilter { private String prefix; public PrefixFilter(String prefix) { this.prefix = prefix; } public boolean accept(File dir, String name) { // look for cert* in this directory return (name.indexOf(prefix + "cert") != -1); } } public static boolean validPrefix(String configDir, String prefix) throws IOException { File dir = new File(configDir); if (!dir.isDirectory()) { System.out.println("ERROR: \"" + dir + "\" is not a directory"); usage(); } String matchingFiles[] = dir.list(new PrefixFilter(prefix)); // prefix may be valid if at least one file matched the pattern return (matchingFiles.length > 0); } public class Result { public int goodSigCount; public int badSigCount; public int sigStartLine; public int sigStopLine; public String sigStartFile; public String sigStopFile; public int signedLines; } public Result verify(List logFiles) throws Exception { PublicKey pubk = signingCert.getPublicKey(); String sigAlgorithm = null; if (pubk instanceof RSAPublicKey) { sigAlgorithm = "SHA-256/RSA"; } else if (pubk instanceof DSAPublicKey) { sigAlgorithm = "SHA-256/DSA"; } else { throw new Exception("Unknown signing certificate key type: " + pubk.getAlgorithm()); } Signature sig = Signature.getInstance(sigAlgorithm, CRYPTO_PROVIDER); sig.initVerify(pubk); int goodSigCount = 0; int badSigCount = 0; int lastFileWritten = -1; int sigStartLine = 1; int sigStopLine = 1; String sigStartFile = logFiles.get(0); String sigStopFile = null; int signedLines = 1; for (int curfile = 0; curfile < logFiles.size(); ++curfile) { String curfileName = logFiles.get(curfile); BufferedReader br = new BufferedReader(new FileReader(curfileName)); if (verbose) { writeFile(curfileName); lastFileWritten = curfile; } String curLine; int linenum = 0; while ((curLine = br.readLine()) != null) { ++linenum; if (curLine.indexOf("AUDIT_LOG_SIGNING") != -1) { if (curfile == 0 && linenum == 1) { // Ignore the first signature of the first file, // since it signs data we don't have access to. if (verbose) { output(linenum, "Ignoring first signature of log series"); } } else { int sigStart = curLine.indexOf("sig: ") + 5; if (sigStart < 5) { output(linenum, "INVALID SIGNATURE"); ++badSigCount; } else { byte[] logSig = base64decode(curLine.substring(sigStart)); // verify the signature if (sig.verify(logSig)) { // signature verifies correctly if (verbose) { writeSigStatus(linenum, sigStartFile, sigStartLine, sigStopFile, sigStopLine, "verification succeeded"); } ++goodSigCount; } else { if (lastFileWritten < curfile) { writeFile(curfileName); lastFileWritten = curfile; } writeSigStatus(linenum, sigStartFile, sigStartLine, sigStopFile, sigStopLine, "VERIFICATION FAILED"); ++badSigCount; } } sig.initVerify(pubk); signedLines = 0; sigStartLine = linenum; sigStartFile = curfileName; } } byte[] lineBytes = curLine.getBytes("UTF-8"); sig.update(lineBytes); sig.update(LINE_SEP_BYTE); ++signedLines; sigStopLine = linenum; sigStopFile = curfileName; } br.close(); } Result result = new Result(); result.goodSigCount = goodSigCount; result.badSigCount = badSigCount; result.sigStartLine = sigStartLine; result.sigStopLine = sigStopLine; result.sigStartFile = sigStartFile; result.sigStopFile = sigStopFile; result.signedLines = signedLines; return result; } public static void main(String args[]) { try { String dbdir = null; String logListFile = null; String signerNick = null; String prefix = null; boolean verbose = false; for (int i = 0; i < args.length; ++i) { if (args[i].equals("-d")) { if (++i >= args.length) usage(); dbdir = args[i]; } else if (args[i].equals("-a")) { if (++i >= args.length) usage(); logListFile = args[i]; } else if (args[i].equals("-n")) { if (++i >= args.length) usage(); signerNick = args[i]; } else if (args[i].equals("-P")) { if (++i >= args.length) usage(); prefix = args[i]; } else if (args[i].equals("-v")) { verbose = true; } else { System.out.println("Unrecognized argument(" + i + "): " + args[i]); usage(); } } if (dbdir == null || logListFile == null || signerNick == null) { System.out.println("Argument omitted"); usage(); } // get list of log files Vector logFiles = new Vector(); BufferedReader r = new BufferedReader(new FileReader(logListFile)); String listLine; while ((listLine = r.readLine()) != null) { StringTokenizer tok = new StringTokenizer(listLine, ","); while (tok.hasMoreElements()) { logFiles.addElement(((String) tok.nextElement()).trim()); } } r.close(); if (logFiles.size() == 0) { System.out.println("Error: no log files listed in " + logListFile); System.exit(1); } // initialize crypto stuff if (prefix == null) { if (!validPrefix(dbdir, "")) { System.out.println("ERROR: \"" + dbdir + "\" does not contain any security databases"); usage(); } CryptoManager.initialize(dbdir); } else { if (!validPrefix(dbdir, prefix)) { System.out.println("ERROR: \"" + prefix + "\" is not a valid prefix"); usage(); } CryptoManager.initialize( new CryptoManager.InitializationValues(dbdir, prefix, prefix, "secmod.db") ); } CryptoManager cm = CryptoManager.getInstance(); X509Certificate signerCert = cm.findCertByNickname(signerNick); AuditVerify verifier = new AuditVerify(); verifier.setVerbose(verbose); verifier.setSigningCert(signerCert); Result result = verifier.verify(logFiles); // Make sure there were no unsigned log entries at the end. // The first signed line is the previous signature, but anything // more than that is data. if (result.signedLines > 1) { System.out.println( "ERROR: log entries after " + result.sigStartFile + ":" + result.sigStartLine + " are UNSIGNED"); result.badSigCount++; } System.out.println("\nVerification process complete."); System.out.println("Valid signatures: " + result.goodSigCount); System.out.println("Invalid signatures: " + result.badSigCount); if (result.badSigCount > 0) { System.exit(2); } else { System.exit(0); } } catch (FileNotFoundException fnfe) { System.out.println(fnfe); } catch (ObjectNotFoundException onfe) { System.out.println("ERROR: certificate not found"); } catch (Exception e) { e.printStackTrace(); } System.out.println("Verification process FAILED."); System.exit(1); } }