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}