View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2015-2016 ForgeRock AS.
15   */
16  package org.forgerock.audit.handlers.csv;
17  
18  import static org.forgerock.audit.handlers.csv.CsvSecureConstants.*;
19  import static org.forgerock.audit.handlers.csv.CsvSecureUtils.*;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.FileReader;
25  import java.io.IOException;
26  import java.security.SignatureException;
27  import java.util.Arrays;
28  import java.util.Map;
29  
30  import javax.crypto.SecretKey;
31  
32  import org.forgerock.audit.secure.SecureStorage;
33  import org.forgerock.audit.secure.SecureStorageException;
34  import org.forgerock.util.encode.Base64;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  import org.supercsv.io.CsvMapReader;
38  import org.supercsv.io.ICsvMapReader;
39  import org.supercsv.prefs.CsvPreference;
40  
41  /**
42   * This class aims to verify a secure CSV file.
43   */
44  class CsvSecureVerifier {
45  
46      private static final Logger logger = LoggerFactory.getLogger(CsvSecureVerifier.class);
47  
48      private File csvFile;
49      private final CsvPreference csvPreference;
50      private final HmacCalculator hmacCalculator;
51      private final SecureStorage secureStorage;
52      private String lastHMAC;
53      private byte[] lastSignature;
54      private String[] headers;
55  
56      /**
57       * Constructs a new verifier.
58       *
59       * @param csvFile
60       *            the CSV file to verify
61       * @param csvPreference
62       *            the CSV preference to use
63       * @param secureStorage
64       *            the secure storage containing keys
65       */
66      public CsvSecureVerifier(File csvFile, CsvPreference csvPreference, SecureStorage secureStorage) {
67          this.csvFile = csvFile;
68          this.csvPreference = csvPreference;
69          this.secureStorage = secureStorage;
70  
71          try {
72              SecretKey initialKey = secureStorage.readInitialKey();
73              if (initialKey == null) {
74                  throw new IllegalStateException("Expecting to find an initial key into the keystore.");
75              }
76  
77              this.hmacCalculator = new HmacCalculator(HMAC_ALGORITHM);
78              this.hmacCalculator.setCurrentKey(initialKey.getEncoded());
79          } catch (SecureStorageException e) {
80              throw new IllegalStateException(e);
81          }
82      }
83  
84      public VerificationResult verify() throws IOException {
85          boolean lastRowWasSigned = false;
86          try (ICsvMapReader csvReader = newBufferedCsvMapReader()) {
87              final String[] header = csvReader.getHeader(true);
88  
89              // Ensure header contains HEADER_HMAC and HEADER_SIGNATURE
90              int checkCount = 0;
91              for (String string : header) {
92                  if (HEADER_HMAC.equals(string) || HEADER_SIGNATURE.equals(string)) {
93                      checkCount++;
94                  }
95              }
96  
97              if (!(HEADER_HMAC.equals(header[header.length - 2])
98                      && HEADER_SIGNATURE.equals(header[header.length - 1]))) {
99                  String msg = "Found only " + checkCount + " checked headers from : " + Arrays.toString(header);
100                 logger.debug(msg);
101                 return newVerificationFailureResult(msg);
102             }
103             this.headers = new String[header.length - 2];
104             System.arraycopy(header, 0, this.headers, 0, this.headers.length);
105 
106             // Check the row one after the other
107             Map<String, String> values;
108             while ((values = csvReader.read(header)) != null) {
109                 logger.trace("Verifying row {}", csvReader.getRowNumber());
110                 lastRowWasSigned = false;
111                 final String encodedSign = values.get(HEADER_SIGNATURE);
112                 // The field HEADER_SIGNATURE is filled so let's check that special row
113                 if (encodedSign != null) {
114                     if (csvReader.getRowNumber() == 2) {
115                         // Special case : this is a rotated file, do not verify the signature but store it.
116                         lastSignature = Base64.decode(encodedSign);
117                     } else if (!verifySignature(encodedSign)) {
118                         String msg = "The signature at row " + csvReader.getRowNumber() + " is not correct.";
119                         logger.trace(msg);
120                         return newVerificationFailureResult(msg);
121                     } else {
122                         logger.trace("The signature at row {} is correct.", csvReader.getRowNumber());
123                         lastRowWasSigned = true;
124                         // The signature is OK : let's continue to the next row
125                         continue;
126                     }
127                 } else {
128                     // Otherwise every row must contain a valid HEADER_HMAC
129                     if (!verifyHMAC(values, header)) {
130                         String msg = "The HMac at row " + csvReader.getRowNumber() + " is not correct.";
131                         logger.trace(msg);
132                         return newVerificationFailureResult(msg);
133                     } else {
134                         logger.trace("The HMac at row {} is correct.", csvReader.getRowNumber());
135                         // The HMAC is OK : let's continue to the next row
136                         continue;
137                     }
138                 }
139             }
140         }
141 
142         try {
143             SecretKey currentKey = secureStorage.readCurrentKey();
144             if (currentKey != null) {
145                 boolean keysMatch = Arrays.equals(hmacCalculator.getCurrentKey().getEncoded(), currentKey.getEncoded());
146                 logger.trace("keysMatch={}, lastRowWasSigned={}", keysMatch, lastRowWasSigned);
147                 if (!keysMatch) {
148                     return newVerificationFailureResult("Final HMAC key doesn't match expected value");
149                 } else if (!lastRowWasSigned) {
150                     return newVerificationFailureResult("Missing final signature");
151                 } else {
152                     return newVerificationSuccessResult();
153                 }
154             } else {
155                 logger.trace("currentKey is null");
156                 return newVerificationFailureResult("Final HMAC key is null");
157             }
158         } catch (SecureStorageException ex) {
159             throw new IOException(ex);
160         }
161     }
162 
163     private CsvMapReader newBufferedCsvMapReader() throws FileNotFoundException {
164         return new CsvMapReader(new BufferedReader(new FileReader(csvFile)), csvPreference);
165     }
166 
167     private VerificationResult newVerificationFailureResult(String msg) {
168         return new VerificationResult(csvFile, false, msg);
169     }
170 
171     private VerificationResult newVerificationSuccessResult() {
172         return new VerificationResult(csvFile, true, "");
173     }
174 
175     private boolean verifyHMAC(Map<String, String> values, String[] header) throws IOException {
176         try {
177             String actualHMAC = values.get(HEADER_HMAC);
178             String expectedHMAC = hmacCalculator.calculate(dataToSign(logger, values, dropExtraHeaders(header)));
179             if (!actualHMAC.equals(expectedHMAC)) {
180                 logger.trace("The HMAC is not valid. Expected : {} Found : {}", expectedHMAC, actualHMAC);
181                 return false;
182             } else {
183                 lastHMAC = actualHMAC;
184                 return true;
185             }
186         } catch (SignatureException ex) {
187             logger.error(ex.getMessage(), ex);
188             throw new IOException(ex);
189         }
190     }
191 
192     private boolean verifySignature(final String encodedSign) throws IOException {
193         try {
194             byte[] signature = Base64.decode(encodedSign);
195             boolean verify = secureStorage.verify(dataToSign(lastSignature, lastHMAC), signature);
196             if (!verify) {
197                 logger.trace("The signature does not match the expecting one.");
198                 return false;
199             } else {
200                 lastSignature = signature;
201                 return true;
202             }
203         } catch (SecureStorageException ex) {
204             logger.error(ex.getMessage(), ex);
205             throw new IOException(ex);
206         }
207     }
208 
209     private String[] dropExtraHeaders(String... header) {
210         // Drop the 2 last headers : HEADER_HMAC and HEADER_SIGNATURE
211         return Arrays.copyOf(header, header.length - 2);
212     }
213 
214     /**
215      * Returns the headers of the underlying CSV.
216      *
217      * @return the headers of the underlying CSV
218      */
219     public String[] getHeaders() {
220         return headers;
221     }
222 
223     /**
224      * Returns the latest read and validated HMAC.
225      *
226      * @return the latest read and validated HMAC
227      */
228     public String getLastHMAC() {
229         return lastHMAC;
230     }
231 
232     /**
233      * Returns the latest read and validated signature.
234      *
235      * @return the latest read and validated signature
236      */
237     public byte[] getLastSignature() {
238         return lastSignature;
239     }
240 
241     static final class VerificationResult {
242 
243         private final File archiveFile;
244         private final boolean passedVerification;
245         private final String failureReason;
246 
247         VerificationResult(final File archiveFile, final boolean passedVerification, final String message) {
248             this.archiveFile = archiveFile;
249             this.passedVerification = passedVerification;
250             this.failureReason = message;
251         }
252 
253         public File getArchiveFile() {
254             return archiveFile;
255         }
256 
257         public boolean hasPassedVerification() {
258             return passedVerification;
259         }
260 
261         public String getFailureReason() {
262             return failureReason;
263         }
264     }
265 }