001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2015-2016 ForgeRock AS. 015 */ 016package org.forgerock.audit.handlers.csv; 017 018import static org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRotation.DEFAULT_ROTATION_FILE_SUFFIX; 019import static org.forgerock.audit.handlers.csv.CsvSecureConstants.KEYSTORE_TYPE; 020 021import java.io.File; 022import java.io.PrintStream; 023import java.nio.file.Path; 024import java.security.PublicKey; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Set; 028 029import javax.crypto.SecretKey; 030 031import org.forgerock.audit.handlers.csv.CsvSecureVerifier.VerificationResult; 032import org.forgerock.audit.retention.FileNamingPolicy; 033import org.forgerock.audit.retention.TimeStampFileNamingPolicy; 034import org.forgerock.audit.secure.JcaKeyStoreHandler; 035import org.forgerock.audit.secure.KeyStoreHandlerDecorator; 036import org.forgerock.audit.secure.KeyStoreSecureStorage; 037import org.forgerock.audit.secure.SecureStorageException; 038import org.forgerock.util.Option; 039import org.forgerock.util.Options; 040import org.forgerock.util.annotations.VisibleForTesting; 041import org.forgerock.util.encode.Base64; 042import org.supercsv.prefs.CsvPreference; 043 044/** 045 * Command line interface for verifying an archived set of tamper evident CSV audit log files for a particular topic. 046 */ 047public class CsvSecureArchiveVerifierCli { 048 049 private static final Option<Path> ARCHIVE_DIRECTORY = Option.of(Path.class, null); 050 private static final Option<String> TOPIC = Option.of(String.class, null); 051 private static final Option<String> PREFIX = Option.of(String.class, ""); 052 private static final Option<String> SUFFIX = Option.of(String.class, DEFAULT_ROTATION_FILE_SUFFIX); 053 private static final Option<Path> KEYSTORE_FILE = Option.of(Path.class, null); 054 private static final Option<String> KEYSTORE_PASSWORD = Option.of(String.class, null); 055 056 @VisibleForTesting 057 static PrintStream out = System.out; 058 @VisibleForTesting 059 static PrintStream err = System.err; 060 @VisibleForTesting 061 static FileNamingPolicyFactory fileNamingPolicyFactory = new DefaultFileNamingPolicyFactory(); 062 063 /** 064 * Entry point for CLI 065 * 066 * @param args command line arguments. 067 */ 068 public static void main(final String[] args) { 069 070 Options options = new OptionsParser(out, err).parse(args); 071 if (options == null) { 072 return; 073 } 074 075 final Path archiveDirectory = options.get(ARCHIVE_DIRECTORY); 076 final String topic = options.get(TOPIC); 077 final File liveFile = new File(archiveDirectory.toFile(), topic + ".csv"); 078 final String prefix = options.get(PREFIX) + CsvAuditEventHandler.SECURE_CSV_FILENAME_PREFIX; 079 final String suffix = options.get(SUFFIX); 080 final FileNamingPolicy fileNamingPolicy = fileNamingPolicyFactory.newFileNamingPolicy(liveFile, suffix, prefix); 081 final Path keystoreFile = options.get(KEYSTORE_FILE); 082 final String keystorePassword = options.get(KEYSTORE_PASSWORD); 083 084 final KeyStoreHandlerDecorator keyStoreHandler = getKeyStoreHandlerDecorator(keystoreFile, keystorePassword); 085 if (keyStoreHandler == null) { 086 return; 087 } 088 089 final PublicKey publicKey = getSignaturePublicKey(keyStoreHandler); 090 if (publicKey == null) { 091 return; 092 } 093 094 final String password = getKeystorePassword(keyStoreHandler); 095 if (password == null) { 096 return; 097 } 098 099 final CsvSecureArchiveVerifier archiveVerifier = 100 new CsvSecureArchiveVerifier(fileNamingPolicy, password, publicKey, CsvPreference.EXCEL_PREFERENCE); 101 final List<CsvSecureVerifier.VerificationResult> verificationResults = archiveVerifier.verify(); 102 103 printVerificationResults(verificationResults, out); 104 } 105 106 private static KeyStoreHandlerDecorator getKeyStoreHandlerDecorator( 107 final Path keystoreFile, final String keystorePassword) { 108 try { 109 return new KeyStoreHandlerDecorator( 110 new JcaKeyStoreHandler(KEYSTORE_TYPE, keystoreFile.toFile().getAbsolutePath(), keystorePassword)); 111 } catch (Exception e) { 112 err.println("Unable to open keystore"); 113 return null; 114 } 115 } 116 117 private static PublicKey getSignaturePublicKey(KeyStoreHandlerDecorator keyStoreHandler) { 118 try { 119 return keyStoreHandler.readPublicKeyFromKeyStore(KeyStoreSecureStorage.ENTRY_SIGNATURE); 120 } catch (SecureStorageException e) { 121 err.println("Unable to read " + KeyStoreSecureStorage.ENTRY_SIGNATURE + " public key from keystore"); 122 return null; 123 } 124 } 125 126 private static String getKeystorePassword(KeyStoreHandlerDecorator keyStoreHandler) { 127 try { 128 final SecretKey passwordKey = keyStoreHandler.readSecretKeyFromKeyStore(CsvSecureConstants.ENTRY_PASSWORD); 129 return Base64.encode(passwordKey.getEncoded()); 130 } catch (SecureStorageException e) { 131 err.println("Unable to read " + CsvSecureConstants.ENTRY_PASSWORD + " secret key from keystore"); 132 return null; 133 } 134 } 135 136 static void printVerificationResults(final List<VerificationResult> verificationResults, final PrintStream out) { 137 for (final VerificationResult verificationResult : verificationResults) { 138 String filename = verificationResult.getArchiveFile().getName(); 139 if (verificationResult.hasPassedVerification()) { 140 out.println("PASS " + filename); 141 } else { 142 out.println("FAIL " + filename + " " + verificationResult.getFailureReason()); 143 } 144 } 145 } 146 147 static final class OptionsParser { 148 149 static final String FLAG_ARCHIVE_DIRECTORY = "--archive"; 150 static final String FLAG_TOPIC = "--topic"; 151 static final String FLAG_PREFIX = "--prefix"; 152 static final String FLAG_SUFFIX = "--suffix"; 153 static final String FLAG_KEYSTORE_FILE = "--keystore"; 154 static final String FLAG_KEYSTORE_PASSWORD = "--password"; 155 156 private static final String DESC_ARCHIVE_DIRECTORY = "path to directory containing files to verify"; 157 private static final String DESC_TOPIC = "name of topic fileset to verify"; 158 private static final String DESC_PREFIX = "prefix prepended to archive files"; 159 private static final String DESC_SUFFIX = "format of timestamp suffix appended to archive files"; 160 private static final String DESC_KEYSTORE_FILE = "path to keystore file"; 161 private static final String DESC_KEYSTORE_PASSWORD = "keystore file password"; 162 163 private final PrintStream out; 164 private final PrintStream err; 165 166 OptionsParser(final PrintStream out, final PrintStream err) { 167 this.out = out; 168 this.err = err; 169 } 170 171 Options parse(final String[] args) { 172 Options options = Options.defaultOptions(); 173 174 if (args.length == 0) { 175 printHelp(); 176 return null; 177 } 178 179 Set<String> flagsSeen = new HashSet<>(); 180 for (int i = 0; i < args.length; i += 2) { 181 final boolean isLastArgument = args.length == i + 1; 182 final String currentArgument = args[i]; 183 if (flagsSeen.contains(currentArgument)) { 184 err.println(currentArgument + " should only be provided once"); 185 return null; 186 } 187 flagsSeen.add(currentArgument); 188 final String nextArgument = isLastArgument ? null : args[i + 1]; 189 switch (currentArgument) { 190 case FLAG_ARCHIVE_DIRECTORY: 191 options.set(ARCHIVE_DIRECTORY, 192 getPathOption(nextArgument, FLAG_ARCHIVE_DIRECTORY, DESC_ARCHIVE_DIRECTORY)); 193 break; 194 case FLAG_TOPIC: 195 options.set(TOPIC, getStringOption(nextArgument, FLAG_TOPIC, DESC_TOPIC)); 196 break; 197 case FLAG_PREFIX: 198 options.set(PREFIX, getStringOption(nextArgument, FLAG_PREFIX, DESC_PREFIX)); 199 break; 200 case FLAG_SUFFIX: 201 options.set(SUFFIX, getStringOption(nextArgument, FLAG_SUFFIX, DESC_SUFFIX)); 202 break; 203 case FLAG_KEYSTORE_FILE: 204 options.set(KEYSTORE_FILE, getPathOption(nextArgument, FLAG_KEYSTORE_FILE, DESC_KEYSTORE_FILE)); 205 break; 206 case FLAG_KEYSTORE_PASSWORD: 207 options.set(KEYSTORE_PASSWORD, 208 getStringOption(nextArgument, FLAG_KEYSTORE_PASSWORD, DESC_KEYSTORE_PASSWORD)); 209 break; 210 default: 211 err.println("Unknown flag " + currentArgument); 212 return null; 213 } 214 } 215 216 if (!flagsSeen.contains(FLAG_ARCHIVE_DIRECTORY) && options.get(ARCHIVE_DIRECTORY) == null) { 217 err.println(DESC_ARCHIVE_DIRECTORY + " must be specified using flag " + FLAG_ARCHIVE_DIRECTORY); 218 return null; 219 } 220 if (!flagsSeen.contains(FLAG_TOPIC) && options.get(TOPIC) == null) { 221 err.println(DESC_TOPIC + " must be specified using flag " + FLAG_TOPIC); 222 return null; 223 } 224 if (!flagsSeen.contains(FLAG_KEYSTORE_FILE) && options.get(KEYSTORE_FILE) == null) { 225 err.println(DESC_KEYSTORE_FILE + " must be specified using flag " + FLAG_KEYSTORE_FILE); 226 return null; 227 } 228 if (!flagsSeen.contains(FLAG_KEYSTORE_PASSWORD) && options.get(KEYSTORE_PASSWORD) == null) { 229 err.println(DESC_KEYSTORE_PASSWORD + " must be specified using flag " + FLAG_KEYSTORE_PASSWORD); 230 return null; 231 } 232 233 return options; 234 } 235 236 private void printHelp() { 237 out.println(String.format("arguments: %s <path> %s <topic> [%s <prefix>] " + 238 "[%s <suffix>] %s <path> %s <password>", FLAG_ARCHIVE_DIRECTORY, FLAG_TOPIC, FLAG_PREFIX, 239 FLAG_SUFFIX, FLAG_KEYSTORE_FILE, FLAG_KEYSTORE_PASSWORD)); 240 out.println(""); 241 out.println(String.format(" %-15s %s", FLAG_ARCHIVE_DIRECTORY, DESC_ARCHIVE_DIRECTORY)); 242 out.println(String.format(" %-15s %s", FLAG_TOPIC, DESC_TOPIC)); 243 out.println(String.format(" %-15s %s", FLAG_PREFIX, DESC_PREFIX)); 244 out.println(String.format(" %-15s %s", FLAG_SUFFIX, DESC_SUFFIX)); 245 out.println(String.format(" %-15s %s", FLAG_KEYSTORE_FILE, DESC_KEYSTORE_FILE)); 246 out.println(String.format(" %-15s %s", FLAG_KEYSTORE_PASSWORD, DESC_KEYSTORE_PASSWORD)); 247 } 248 249 private Path getPathOption(String nextArgument, String flag, String description) { 250 if (nextArgument == null) { 251 err.println(flag + " flag must be followed by " + description); 252 return null; 253 } 254 final File file = new File(nextArgument); 255 if (!file.exists()) { 256 err.println(file + " not found"); 257 return null; 258 } 259 return file.toPath(); 260 } 261 262 private String getStringOption(String nextArgument, String flag, String description) { 263 if (nextArgument == null) { 264 err.println(flag + " flag must be followed by " + description); 265 return null; 266 } 267 return nextArgument; 268 } 269 270 } 271 272 /** 273 * This interface exists solely to allow tests to replace the default FileNamingPolicy used by {@link #main}. 274 * <br/> 275 * The default policy {@link TimeStampFileNamingPolicy} sorts files by their timestamp meta-data but this is only 276 * accurate to the nearest second and therefore doesn't correctly sort the files generated by tests (as multiple 277 * files can be created within a single second). 278 */ 279 interface FileNamingPolicyFactory { 280 281 FileNamingPolicy newFileNamingPolicy(File liveFile, String suffix, String prefix); 282 283 } 284 285 static class DefaultFileNamingPolicyFactory implements FileNamingPolicyFactory { 286 287 @Override 288 public FileNamingPolicy newFileNamingPolicy(File liveFile, String suffix, String prefix) { 289 return new TimeStampFileNamingPolicy(liveFile, suffix, prefix); 290 } 291 } 292}