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}