001/*
002 * Copyright (C) 2016 Hobrasoft s.r.o.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU Affero General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU Affero General Public License for more details.
013 *
014 * You should have received a copy of the GNU Affero General Public License
015 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
016 */
017package cz.hobrasoft.pdfmu.operation;
018
019import com.itextpdf.text.pdf.AcroFields;
020import com.itextpdf.text.pdf.PdfReader;
021import com.itextpdf.text.pdf.security.CertificateInfo;
022import com.itextpdf.text.pdf.security.CertificateInfo.X500Name;
023import com.itextpdf.text.pdf.security.PdfPKCS7;
024import cz.hobrasoft.pdfmu.MapSorter;
025import cz.hobrasoft.pdfmu.PreferenceListComparator;
026import cz.hobrasoft.pdfmu.jackson.CertificateResult;
027import cz.hobrasoft.pdfmu.jackson.Inspect;
028import cz.hobrasoft.pdfmu.jackson.Signature;
029import cz.hobrasoft.pdfmu.jackson.SignatureDisplay;
030import cz.hobrasoft.pdfmu.jackson.SignatureMetadata;
031import cz.hobrasoft.pdfmu.operation.args.InPdfArgs;
032import cz.hobrasoft.pdfmu.operation.metadata.MetadataParameters;
033import cz.hobrasoft.pdfmu.operation.version.PdfVersion;
034import java.io.File;
035import java.io.FileInputStream;
036import java.io.IOException;
037import java.io.InputStream;
038import java.security.cert.Certificate;
039import java.security.cert.X509Certificate;
040import java.util.ArrayList;
041import java.util.Date;
042import java.util.HashMap;
043import java.util.LinkedHashMap;
044import java.util.List;
045import java.util.Map;
046import java.util.Map.Entry;
047import java.util.SortedMap;
048import javax.security.auth.x500.X500Principal;
049import net.sourceforge.argparse4j.inf.Namespace;
050import net.sourceforge.argparse4j.inf.Subparser;
051import org.apache.commons.lang3.StringUtils;
052
053/**
054 *
055 * @author Filip Bartek
056 */
057public class OperationInspect extends OperationCommon {
058
059    private final InPdfArgs in = new InPdfArgs();
060
061    @Override
062    public Subparser configureSubparser(Subparser subparser) {
063        String help = "Display PDF version, properties and signatures of a PDF document";
064
065        // Configure the subparser
066        subparser.help(help)
067                .description(help)
068                .defaultHelp(true);
069
070        in.addArguments(subparser);
071
072        return subparser;
073    }
074
075    @Override
076    public void execute(Namespace namespace) throws OperationException {
077        in.setFromNamespace(namespace);
078        in.open();
079        PdfReader pdfReader = in.getPdfReader();
080        Inspect result;
081        try {
082            result = execute(pdfReader);
083        } finally {
084            in.close();
085        }
086        writeResult(result);
087    }
088
089    public Inspect execute(File file) throws OperationException, IOException {
090        assert file != null;
091        Inspect result;
092        try (InputStream is = new FileInputStream(file)) {
093            PdfReader pdfReader = new PdfReader(is);
094            try {
095                result = execute(pdfReader);
096            } finally {
097                pdfReader.close();
098            }
099        }
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}