View Javadoc
1   /*
2    * Copyright (C) 2016 Hobrasoft s.r.o.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package cz.hobrasoft.pdfmu.operation;
18  
19  import com.itextpdf.text.pdf.AcroFields;
20  import com.itextpdf.text.pdf.PdfReader;
21  import com.itextpdf.text.pdf.security.CertificateInfo;
22  import com.itextpdf.text.pdf.security.CertificateInfo.X500Name;
23  import com.itextpdf.text.pdf.security.PdfPKCS7;
24  import cz.hobrasoft.pdfmu.MapSorter;
25  import cz.hobrasoft.pdfmu.PreferenceListComparator;
26  import cz.hobrasoft.pdfmu.jackson.CertificateResult;
27  import cz.hobrasoft.pdfmu.jackson.Inspect;
28  import cz.hobrasoft.pdfmu.jackson.Signature;
29  import cz.hobrasoft.pdfmu.jackson.SignatureDisplay;
30  import cz.hobrasoft.pdfmu.jackson.SignatureMetadata;
31  import cz.hobrasoft.pdfmu.operation.args.InPdfArgs;
32  import cz.hobrasoft.pdfmu.operation.metadata.MetadataParameters;
33  import cz.hobrasoft.pdfmu.operation.version.PdfVersion;
34  import java.io.File;
35  import java.io.FileInputStream;
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.security.cert.Certificate;
39  import java.security.cert.X509Certificate;
40  import java.util.ArrayList;
41  import java.util.Date;
42  import java.util.HashMap;
43  import java.util.LinkedHashMap;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Map.Entry;
47  import java.util.SortedMap;
48  import javax.security.auth.x500.X500Principal;
49  import net.sourceforge.argparse4j.inf.Namespace;
50  import net.sourceforge.argparse4j.inf.Subparser;
51  import org.apache.commons.lang3.StringUtils;
52  
53  /**
54   *
55   * @author Filip Bartek
56   */
57  public class OperationInspect extends OperationCommon {
58  
59      private final InPdfArgs in = new InPdfArgs();
60  
61      @Override
62      public Subparser configureSubparser(Subparser subparser) {
63          String help = "Display PDF version, properties and signatures of a PDF document";
64  
65          // Configure the subparser
66          subparser.help(help)
67                  .description(help)
68                  .defaultHelp(true);
69  
70          in.addArguments(subparser);
71  
72          return subparser;
73      }
74  
75      @Override
76      public void execute(Namespace namespace) throws OperationException {
77          in.setFromNamespace(namespace);
78          in.open();
79          PdfReader pdfReader = in.getPdfReader();
80          Inspect result;
81          try {
82              result = execute(pdfReader);
83          } finally {
84              in.close();
85          }
86          writeResult(result);
87      }
88  
89      public Inspect execute(File file) throws OperationException, IOException {
90          assert file != null;
91          Inspect result;
92          try (InputStream is = new FileInputStream(file)) {
93              PdfReader pdfReader = new PdfReader(is);
94              try {
95                  result = execute(pdfReader);
96              } finally {
97                  pdfReader.close();
98              }
99          }
100         return result;
101     }
102 
103     private Inspect execute(PdfReader pdfReader) throws OperationException {
104         Inspect result = new Inspect();
105 
106         // Fetch the PDF version of the input PDF document
107         PdfVersion inVersion = new PdfVersion(pdfReader.getPdfVersion());
108         to.println(String.format("PDF version: %s", inVersion));
109         result.version = inVersion.toString();
110 
111         result.properties = get(pdfReader);
112 
113         result.signatures = display(pdfReader);
114 
115         return result;
116     }
117 
118     private SortedMap<String, String> get(PdfReader pdfReader) {
119         Map<String, String> properties = pdfReader.getInfo();
120 
121         MetadataParameters mp = new MetadataParameters();
122         mp.setFromInfo(properties);
123 
124         SortedMap<String, String> propertiesSorted = mp.getSorted();
125 
126         {
127             to.indentMore("Properties:");
128             for (Map.Entry<String, String> property : propertiesSorted.entrySet()) {
129                 String key = property.getKey();
130                 String value = property.getValue();
131                 to.println(String.format("%s: %s", key, value));
132             }
133             to.indentLess();
134         }
135 
136         return propertiesSorted;
137     }
138 
139     public SignatureDisplay display(PdfReader pdfReader) {
140         // digitalsignatures20130304.pdf : Code sample 5.1
141         AcroFields fields = pdfReader.getAcroFields();
142         return display(fields);
143     }
144 
145     private SignatureDisplay display(AcroFields fields) {
146         SignatureDisplay result = new SignatureDisplay();
147 
148         // digitalsignatures20130304.pdf : Code sample 5.1
149         ArrayList<String> names = fields.getSignatureNames();
150 
151         // Print number of signatures
152         to.println(String.format("Number of signatures: %d", names.size()));
153         to.println(String.format("Number of document revisions: %d", fields.getTotalRevisions()));
154         result.nRevisions = fields.getTotalRevisions();
155 
156         List<Signature> signatures = new ArrayList<>();
157 
158         for (String name : names) {
159             to.println(String.format("Signature field name: %s", name));
160 
161             to.indentMore();
162             Signature signature;
163             try {
164                 signature = display(fields, name); // May throw OperationException
165             } finally {
166                 to.indentLess();
167             }
168             signature.id = name;
169             signatures.add(signature);
170         }
171 
172         result.signatures = signatures;
173 
174         return result;
175     }
176 
177     private Signature display(AcroFields fields, String name) {
178         // digitalsignatures20130304.pdf : Code sample 5.2
179         to.println(String.format("Signature covers the whole document: %s", (fields.signatureCoversWholeDocument(name) ? "Yes" : "No")));
180         to.println(String.format("Document revision: %d of %d", fields.getRevision(name), fields.getTotalRevisions()));
181 
182         PdfPKCS7 pkcs7 = fields.verifySignature(name);
183         Signature signature = display(pkcs7);
184         signature.coversWholeDocument = fields.signatureCoversWholeDocument(name);
185         signature.revision = fields.getRevision(name);
186         return signature;
187     }
188 
189     private Signature display(PdfPKCS7 pkcs7) {
190         Signature signature = new Signature();
191 
192         // digitalsignatures20130304.pdf : Code sample 5.3
193         to.println("Signature metadata:");
194         {
195             SignatureMetadata metadata = new SignatureMetadata();
196 
197             to.indentMore();
198 
199             // Only name may be null.
200             // The values are set in {@link PdfPKCS7#verifySignature}.
201             { // name
202                 String name = pkcs7.getSignName(); // May be null
203                 metadata.name = name;
204                 if (name == null) {
205                     to.println("Name is not set.");
206                 } else {
207                     to.println(String.format("Name: %s", name));
208                 }
209             }
210 
211             // TODO?: Print "N/A" if the value is an empty string
212             // TODO?: Determine whether the value is set in the signature
213             to.println(String.format("Reason: %s", pkcs7.getReason()));
214             metadata.reason = pkcs7.getReason();
215             to.println(String.format("Location: %s", pkcs7.getLocation()));
216             metadata.location = pkcs7.getLocation();
217 
218             { // Date
219                 Date date = pkcs7.getSignDate().getTime();
220                 to.println(String.format("Date and time: %s", date));
221                 metadata.date = date.toString();
222             }
223 
224             to.indentLess();
225 
226             signature.metadata = metadata;
227         }
228         { // Certificate chain
229             to.indentMore("Certificate chain:");
230             Certificate[] certificates = pkcs7.getSignCertificateChain();
231             to.println(String.format("Number of certificates: %d", certificates.length));
232             int i = 0;
233             List<CertificateResult> certificatesResult = new ArrayList<>();
234             for (Certificate certificate : certificates) {
235                 to.indentMore(String.format("Certificate %d%s:", i, (i == 0 ? " (the signing certificate)" : "")));
236                 CertificateResult certRes;
237                 String type = certificate.getType();
238                 to.println(String.format("Type: %s", type));
239                 // http://docs.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html#AppA
240                 if ("X.509".equals(type)) {
241                     X509Certificate certificateX509 = (X509Certificate) certificate;
242                     certRes = showCertInfo(certificateX509);
243                 } else {
244                     certRes = new CertificateResult();
245                 }
246                 certRes.type = type;
247                 to.indentLess();
248                 certificatesResult.add(certRes);
249                 ++i;
250             }
251             signature.certificates = certificatesResult;
252             to.indentLess();
253         }
254 
255         return signature;
256     }
257 
258     private CertificateResult showCertInfo(X509Certificate cert) {
259         CertificateResult certRes = new CertificateResult();
260 
261         { // Self-signed?
262             X500Principal principalSubject = cert.getSubjectX500Principal();
263             X500Principal principalIssuer = cert.getIssuerX500Principal();
264             boolean selfSigned = principalSubject.equals(principalIssuer);
265             to.println(String.format("Self-signed: %s", (selfSigned ? "Yes" : "No")));
266             certRes.selfSigned = selfSigned;
267         }
268 
269         // Note: More attributes may be available by more direct processing of `cert`
270         // than by using `CertificateInfo.get*Fields`.
271         { // Subject
272             to.indentMore("Subject:");
273             certRes.subject = showX500Name(CertificateInfo.getSubjectFields(cert));
274             to.indentLess();
275         }
276         { // Issuer
277             to.indentMore("Issuer:");
278             certRes.issuer = showX500Name(CertificateInfo.getIssuerFields(cert));
279             to.indentLess();
280         }
281 
282         return certRes;
283     }
284 
285     // The desired order of DN attributes by their type
286     private static final MapSorter<String> dnTypeSorter = new PreferenceListComparator(new String[]{
287         "CN", "E", "OU", "O", "STREET", "L", "ST", "C"});
288 
289     /**
290      * The returned map is ordered by keys by {@link dnTypeSorter}.
291      */
292     private SortedMap<String, List<String>> showX500Name(X500Name name) {
293         Map<String, ArrayList<String>> fields = name.getFields();
294 
295         // Convert to Map<String, List<String>>
296         Map<String, List<String>> fieldsLists = new LinkedHashMap<>();
297         fieldsLists.putAll(fields);
298 
299         // Sort by dnTypeSorter
300         SortedMap<String, List<String>> fieldsSorted = dnTypeSorter.sort(fieldsLists);
301 
302         // Print
303         for (Entry<String, List<String>> field : fieldsSorted.entrySet()) {
304             String type = field.getKey();
305             type = niceX500AttributeType(type);
306             List<String> values = field.getValue();
307             String valuesString = StringUtils.join(values, ", ");
308             to.println(String.format("%s: %s", type, valuesString));
309         }
310 
311         return fieldsSorted;
312     }
313 
314     private static final Map<String, String> attributeTypeAliases = new HashMap<>();
315 
316     static {
317         // Alias sources:
318         // http://www.ietf.org/rfc/rfc2253.txt : Section 2.3
319         // http://api.itextpdf.com/itext/com/itextpdf/text/pdf/security/CertificateInfo.X500Name.html
320         attributeTypeAliases.put("CN", "Common name");
321         attributeTypeAliases.put("L", "Locality");
322         attributeTypeAliases.put("ST", "State or province");
323         attributeTypeAliases.put("O", "Organization");
324         attributeTypeAliases.put("OU", "Organizational unit");
325         attributeTypeAliases.put("C", "Country code");
326         attributeTypeAliases.put("STREET", "Street address");
327         attributeTypeAliases.put("DC", "Domain component");
328         attributeTypeAliases.put("UID", "User ID");
329         attributeTypeAliases.put("E", "Email address");
330         attributeTypeAliases.put("SN", "Serial number");
331         attributeTypeAliases.put("T", "Title");
332     }
333 
334     private static String niceX500AttributeType(String type) {
335         String nice = attributeTypeAliases.get(type);
336         if (nice != null) {
337             type = nice;
338         } else {
339             return String.format("<%s>", type);
340         }
341 
342         return type;
343     }
344 
345     private static OperationInspect instance = null;
346 
347     public static OperationInspect getInstance() {
348         if (instance == null) {
349             instance = new OperationInspect();
350         }
351         return instance;
352     }
353 
354     private OperationInspect() {
355         // Singleton
356     }
357 
358 }