PDF

PDF訊息摘要驗證

邱誌寬 2020/11/27 16:57:04
212

基於數位驗證的重要性,本篇提供以金鑰來增加是否為正本檔案或者出自於同一來源的驗證,

本篇將產生帶有訊息摘要PDF檔案並對其進行驗證。

 

訊息摘要是將檔案或文字進行雜奏(Hash)演算法處理後所產生出來的值,每個案檔所產生的值皆不相同。

 

以下範例會有內部註解來解釋目前程式碼階段是做什麼的:

1.將現有的PDF加上簽章,並產出帶有簽章的PDF。

/**
	 * 
	 * @param src             需要簽章的pdf檔案路徑
	 * @param dest            簽完章的pdf檔案路徑
	 * @param chain           證書鏈
	 * @param pk              簽名私鑰
	 * @param digestAlgorithm 摘要演算法名稱,例如SHA-1
	 * @param provider        金鑰演算法提供者,可以為null
	 * @param subfilter       數字簽名格式,itext有2種
	 * @param location        簽名的地點,顯示在pdf簽名屬性中,隨便填
	 * @param filePath        簽名名稱
	 */
	public void sign(String src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm,
			String provider, CryptoStandard subfilter, String location, String fieldName, ReceiptPdfBean receipt) {

		try {
			PdfReader reader = new PdfReader(src);
			FileOutputStream os = new FileOutputStream(dest);

			// 建立簽章工具PdfStamper ,最後一個boolean引數
			// false的話,pdf檔案只允許被簽名一次,多次簽名,最後一次有效
			// true的話,pdf可以被追加簽名,驗籤工具可以識別出每次簽名之後文件是否被修改
			PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0', null, true);

			// 獲取數字簽章屬性物件,設定數字簽章的屬性
			PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
			appearance.setLocation(location);

			// 設定簽名的位置,頁碼,簽名域名稱,多次追加簽名的時候,簽名預名稱不能一樣
			// 簽名的位置,是圖章相對於pdf頁面的位置座標,原點為pdf頁面左下角
			// 四個引數的分別是,圖章左下角x,圖章左下角y,圖章右上角x,圖章右上角y
			appearance.setVisibleSignature(new Rectangle(80, 70, 60, 20), 1, fieldName);
			// 讀取圖章圖片,這個image是itext包的image
			Image image = Image.getInstance(images.getURL());
			appearance.setSignatureGraphic(image);
			appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
			appearance.setRenderingMode(RenderingMode.GRAPHIC);
			appearance.setCertificate(chain[0]);
			// 摘要演算法
			ExternalDigest digest = new BouncyCastleDigest();

			// 簽名演算法
			ExternalSignature signature = new PrivateKeySignature(pk, digestAlgorithm, null);

			// 呼叫itext簽名方法完成pdf簽章
			MakeSignature.signDetached(appearance, digest, signature, chain, null, null, null, 0, subfilter);
			os.close();
			reader.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (DocumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (GeneralSecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

2.對帶有簽章PDF進行驗證,以確認PDF並未遭到串改。

@RequestMapping(value = "PDF", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE, headers = "Content-Type= multipart/form-data")
	@ResponseBody
	public List<PDFResponseBean> pdfUpload(@RequestParam("file") MultipartFile[] uploadfiles) {

		List<PDFResponseBean> list = new ArrayList<>();
		System.out.println("上傳程式測試" + uploadfiles.length);
		for (MultipartFile multipartFile : uploadfiles) {
			System.out.println("============================================================================");
			File convFile = new File(multipartFile.getOriginalFilename());
			String fileTyle = convFile.getName().substring(convFile.getName().lastIndexOf("."),
					convFile.getName().length());
			System.out.println(convFile.getName());

			try {
				// 驗證檔案是否為pdf
				if (!fileTyle.equals(".pdf")) {
					list.add(new PDFResponseBean(convFile.getName(), false, "is not .pdf file", null));
					System.out.println("驗證檔案是否為pdf=fail");
					continue;
				}

				/*
				 * 摘要屬性為文件類身分證
				 */
				ExternalDigest externalDigest = new ExternalDigest() {
					public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException {
						return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
					}
				};
				InputStream data = multipartFile.getInputStream();

				byte hash[] = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));

				String fileDigestAlgorithms = new String(Base64.encode(hash), Charsets.UTF_8);// 取出的文件摘要屬性

				System.out.println("檔案摘要屬性:" + fileDigestAlgorithms);

				// 驗證P12金鑰

				String PASSWORDSTRING = "8888";// 金鑰密碼
				// char[] PASSWORD = PASSWORDSTRING.toCharArray();// 金鑰編譯密碼

				// ========================金鑰內文提取ST========================
				// File keyfile = BaseConfigUtils.keysTore.getFile();

				Pkcs12Util util = new Pkcs12Util(BaseConfigUtils.keysTore.getInputStream(), PASSWORDSTRING);

				System.out.println("Is certificate valid : " + util.isValidCert());
				System.out.println("Issuer details : " + util.getIssuerName());
				// System.out.println("Full details : " + util.getDetails());
				// ========================金鑰內文提取ED========================

				// ======================檔案金鑰內文提取ST=======================
				PdfReader reader = new PdfReader(multipartFile.getInputStream());

				System.out.println(multipartFile.getOriginalFilename());

				AcroFields acro = reader.getAcroFields();

				List<String> names = acro.getSignatureNames();

				ByteArrayOutputStream out = new ByteArrayOutputStream();
				byte bb[] = new byte[8192];
				InputStream ip = acro.extractRevision(names.get(0));
				int n = 0;
				while ((n = ip.read(bb)) > 0)
					out.write(bb, 0, n);
				out.close();
				ip.close();
				MessageDigest md = MessageDigest.getInstance("SHA-256");
				byte[] resum = md.digest(out.toByteArray());

				String fileDigestAlgorithms2 = new String(Base64.encode(resum), Charsets.UTF_8);// 取出金鑰的文件摘要屬性
				System.out.println("文件檔案摘要屬性:" + fileDigestAlgorithms);
				System.out.println("金鑰檔案摘要屬性:" + fileDigestAlgorithms2);

				if (!StringUtils.equals(fileDigestAlgorithms2, fileDigestAlgorithms)) {
					list.add(new PDFResponseBean(convFile.getName(), false, "verification failed", null));
					System.out.println("檔案摘要屬性fail");
					continue;
				}

				System.out.println("getBlankSignatureNames:" + acro.getBlankSignatureNames());
				System.out.println("getFields:" + acro.getFields());

				/*
				 * 驗證金鑰簽名名稱 signatureCoversWholeDocumentName
				 */
				String signatureCoversWholeDocumentName = "sig";

				Security.addProvider(new BouncyCastleProvider());// PdfPKCS7 必要注入程序 要早於PdfPKCS7先行
				PdfPKCS7 pk = acro.verifySignature(signatureCoversWholeDocumentName);
				System.out.println(
						"checkinKey-Object:" + acro.signatureCoversWholeDocument(signatureCoversWholeDocumentName));
				if (!acro.signatureCoversWholeDocument(signatureCoversWholeDocumentName)) {
					list.add(new PDFResponseBean(convFile.getName(), false, "verification failed", null));
					System.out.println("驗證金鑰簽名名稱fail");
					continue;
				}

				System.out.println("Subject: " + CertificateInfo.getSubjectFields(pk.getSigningCertificate()));
				// ======================檔案金鑰內文提取ED=======================

				/*
				 * 驗證金鑰內備註屬性
				 */
				String ST = CertificateInfo.getSubjectFields(pk.getSigningCertificate()).getField("ST");
				String C = CertificateInfo.getSubjectFields(pk.getSigningCertificate()).getField("C");
				String L = CertificateInfo.getSubjectFields(pk.getSigningCertificate()).getField("L");

				System.out.println("ST:" + ST + "/C:" + C);

				if (util.getIssuerName().indexOf("ST=" + ST + "") == -1) {
					list.add(new PDFResponseBean(convFile.getName(), false, "verification failed", null));
					System.out.println("驗證金鑰內備註屬性fail=ST");
					continue;
				}

				if (util.getIssuerName().indexOf("C=" + C) == -1) {
					list.add(new PDFResponseBean(convFile.getName(), false, "verification failed", null));
					System.out.println("驗證金鑰內備註屬性fail=C");
					continue;
				}

				if (util.getIssuerName().indexOf("L=" + L) == -1) {
					list.add(new PDFResponseBean(convFile.getName(), false, "verification failed", null));
					System.out.println("驗證金鑰內備註屬性fail=L");
					continue;
				}

			} catch (Exception e) {
				list.add(new PDFResponseBean(convFile.getName(), false, "verification failed", null));
				e.printStackTrace();
			}

		}

		return list;
	}

3.使用Postman將檔案上傳進行驗證

以下執行結果會針對金鑰內容以及依照金鑰製作的簽章來進行驗證結果的顯示,如果該內容不和

原簽章內容或者金鑰批配不上,則會顯示不合法檔案的log

 

執行結果:

 

邱誌寬