【问题标题】:iText 5 - Add empty signature field to a digitally signed document without breaking the signiText 5 - 在不破坏签名的情况下向数字签名文档添加空签名字段
【发布时间】:2021-05-27 17:05:31
【问题描述】:

我正在尝试将一个空的签名字段添加到现有的数字签名 pdf(验证签名)中。

我有一个工作流程,其中许多用户将签署文档(批准签名),该文档是用“n”个空签名字段创建的,每个用户一个,我们的应用程序首先应用一个不可见的证书签名,然后每个用户都可以签名相应字段中的文档,但由于工作流程中的意外更改,其他用户可能想要签名,因此我们希望添加相应的空签名字段,然后应用签名。

我尝试将空字段(带有单元格事件的表格)添加到认证文档,但是当我想添加它并关联该字段时,它破坏了签名,我无法使其正常工作。

这里是用于签名、添加签名字段和设置签名字段选项的方法。 我不知道我做错了什么。

public static String sign(SignRequest signRequest, File certificate, File unsignedDocument, File image, File icon)
        throws FileNotFoundException, IOException, DocumentException, StreamParsingException, OCSPException,
        OperatorException, URISyntaxException, WriterException, GeneralSecurityException, FontFormatException {

    SignatureType sigType = Optional
            .ofNullable(SignatureType.get(signRequest.getSignatureProperties().getSignatureType()))
            .orElse(SignatureType.APPROVAL_SIGNATURE);

    File signedDocument = File.createTempFile("signed",".pdf");
    char[] pass = signRequest.getKeyStore().getPassword().toCharArray();

    // Load certificate chain
    BouncyCastleProvider provider = new BouncyCastleProvider();
    Security.addProvider(provider);
    KeyStore ks = KeyStore.getInstance("PKCS12", provider.getName());
    ks.load(new FileInputStream(certificate.getAbsolutePath()), pass);

    String alias = getAliasFromKeyStore(ks);
    PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
    Certificate[] chain = ks.getCertificateChain(alias);

    // Creating the reader and the stamper
    PdfReader reader = new PdfReader(FileUtils.openInputStream(unsignedDocument));
    FileOutputStream os = new FileOutputStream(signedDocument);
    PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0', null, true);
    PdfSignatureAppearance appearance = null;

    // Certify o approval signature (approval is the default signature type)
    switch (sigType) {
    case CERTIFY_SIGNATURE:
        if (reader.getAcroFields().getSignatureNames().size() <= 0) {
            appearance = setSignatureFieldOptions(stamper.getSignatureAppearance(), reader, chain,
                    signRequest, image, icon, Boolean.TRUE);
        } else {
            appearance = setSignatureFieldOptions(stamper.getSignatureAppearance(), reader, chain,
                    signRequest, image, icon, Boolean.FALSE);
        }
        break;
    case APPROVAL_SIGNATURE:
    default:
        appearance = setSignatureFieldOptions(stamper.getSignatureAppearance(), reader, chain, signRequest,
                image, icon, Boolean.FALSE);
        break;
    }

    // Adding LTV (optional)
    OcspClient ocspClient = null;
    List<CrlClient> crlList = null;
    if (signRequest.getSignatureProperties().getLtv() == Boolean.TRUE) {
        ocspClient = new OcspClientBouncyCastle(new OCSPVerifier(null, null));
        CrlClient crlClient = new CrlClientOnline(chain);
        crlList = new ArrayList<CrlClient>();
        crlList.add(crlClient);
    }

    // Adding timestamp (optional)
    TSAClient tsaClient = null;
    if (signRequest.getTimestamp() != null
            && StringUtils.isNotBlank(signRequest.getTimestamp().getUrl())) {
        tsaClient = new TSAClientBouncyCastle(signRequest.getTimestamp().getUrl(),
                signRequest.getTimestamp().getUser(), signRequest.getTimestamp().getPassword());
    }

    // Creating the signature
    ExternalSignature pks = new PrivateKeySignature(pk, signtRequest.getSignatureProperties().getAlgorithm(),
            provider.getName());
    ExternalDigest digest = new BouncyCastleDigest();

    MakeSignature
            .signDetached(appearance, digest, pks, chain, crlList, ocspClient, tsaClient,
                    calculateEstimatedSize(chain, ocspClient, tsaClient, crlList, getEstimatedSizeBonus()), CryptoStandard.CMS);

    return signedDocument.getAbsolutePath();
}


private static PdfSignatureAppearance setSignatureFieldOptions(PdfSignatureAppearance appearance, PdfReader reader, Certificate[] chain, SignRequest signRequest, File image, File icon, Boolean certifySignature) throws MalformedURLException, IOException, DocumentException {
    SignatureProperties sigProperties = signRequest.getSignatureProperties();
    SignatureField sigField = sigProperties.getSignatureField();

    // Creating the appearance
    appearance.setSignatureCreator(Constant.SIGNATURE_CREATOR);
    Optional.ofNullable(sigProperties.getReason()).ifPresent(appearance::setReason);
    Optional.ofNullable(sigProperties.getLocation()).ifPresent(appearance::setLocation);

    if (certifySignature) {
        appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
    } else {
        appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
    }


    /**
     * Signature Field Name
     */
    BoundingBox box = sigProperties.getSignatureField().getBoundingBox();
    String fieldName = sigField.getName();
    int pageNumber = sigProperties.getSignatureField().getPage();

    if (!sigField.isVisible()) {
        if (StringUtils.isBlank(sigField.getName())) {
            fieldName = generateFieldName();
            appearance.setVisibleSignature(new Rectangle(0, 0, 0, 0), pageNumber, fieldName);
        } else {
            appearance.setVisibleSignature(new Rectangle(0, 0, 0, 0), pageNumber, fieldName);
        }
    } else {
        Font font = FontFactory.getFont(Optional.ofNullable(sigField.getFontName()).orElse(BaseFont.HELVETICA),
                Optional.ofNullable(sigField.getFontSize()).orElse(6));
        Rectangle rect = null;
        FieldPosition fieldPosition = null;
        
         //ADD EMPTY FIELD
        if (StringUtils.isBlank(sigField.getName()) && box != null) {
            fieldName = generateFieldName();
            rect = new Rectangle(box.getLowerLeftX(), box.getLowerLeftY(), box.getLowerLeftX() + box.getWidth(),
                    box.getLowerLeftY() + box.getHeight());
            appearance.setVisibleSignature(rect, pageNumber, fieldName);

            ////////////////////////////////// TRY TO ADD EXTRA SIGNATURE FIELD///////////////////////////////////////////
            Rectangle documentRectangle = reader.getPageSize(pageNumber);
            PdfStamper stamper = appearance.getStamper();

            float pageMargin = 10;
            float tableMargin = 15;
            int numberOfFields = 1; // 1 sigField

            float headerWidth = (documentRectangle.getWidth() - (pageMargin * 2));

            // Table with signature field
            PdfPTable table = new PdfPTable(1);
            table.setTotalWidth(headerWidth - (tableMargin * 4));
            table.setLockedWidth(Boolean.TRUE);
            table.setWidthPercentage(100);
            float posXTable = (pageMargin + (headerWidth - table.getTotalWidth()) / 2);
            float posYTable = 400; // custom y position
            int height = 70; // custom height

            for (int i = 0; i < numberOfFields; i++) {
                String sigFieldName = String.format(Constant.SIGNATURE_FIELD_PREFIX + "%s", (i + 1));
                table.addCell(createSignatureFieldCell(stamper, sigFieldName, height, pageNumber));
            }
            table.writeSelectedRows(0, -1, posXTable, posYTable, stamper.getOverContent(pageNumber));

            ////////////////////////////////// END TRY TO ADD EXTRA SIGNATURE FIELD///////////////////////////////////////////

        } else {
            //APPLY SIGNATURE TO EXISTING EMPTY FIELD
            List<FieldPosition> acroFields = reader.getAcroFields().getFieldPositions(sigField.getName());

            fieldPosition = acroFields.get(0);
            appearance.setVisibleSignature(fieldName);
        }

        // --------------------------- Custom signature appearance ---------------------
        PdfTemplate t = appearance.getLayer(2);
        Rectangle sigRect = null;

        if (fieldPosition != null) {
            sigRect = fieldPosition.position;
        } else {
            sigRect = new Rectangle(box.getLowerLeftX(), box.getLowerLeftY(), box.getLowerLeftX() + box.getWidth(),
                    box.getLowerLeftY() + box.getHeight());
        }

        // Left rectangle
        Rectangle leftRect = new Rectangle(0, 0, (sigRect.getWidth() / 5), (sigRect.getHeight() / 2));
        ColumnText ct1 = new ColumnText(t);
        ct1.setSimpleColumn(leftRect);

        Image im1 = Image.getInstance(icon.getAbsolutePath());
        float ratio1 = leftRect.getHeight() / im1.getHeight();
        im1.scaleToFit(im1.getWidth() * ratio1, im1.getHeight() * ratio1);

        Paragraph p = createParagraph("Digital sign", font, Constant.PARAGRAPH_LEADING, Constant.MARGIN * 9);

        ct1.addElement(new Chunk(im1, Constant.MARGIN * 10, 0));
        ct1.addElement(p);
        ct1.go();

        // Middle rectangle
        Rectangle middleRect = new Rectangle((sigRect.getWidth() / 5), 0,
                (leftRect.getWidth() + sigRect.getWidth() / 5), (sigRect.getHeight() / 2));
        ColumnText ct2 = new ColumnText(t);
        ct2.setSimpleColumn(middleRect);

        if (visibleSignatureImage != null) {
            Image im2 = Image.getInstance(image.getAbsolutePath());
            float ratio2 = sigRect.getHeight() / im2.getHeight();
            im2.scaleToFit(im2.getWidth() * ratio2, im2.getHeight() * ratio2);
            ct2.addElement(new Chunk(im2, 0, 0));
            ct2.go();
        }

        // TextFields
        List<TextField> textFields = fillSignatureFieldText(chain, sigProperties, font);

        // Right rectangle - Names
        Rectangle rightRectNames = new Rectangle(
                (Constant.MARGIN * 5 + leftRect.getWidth() + middleRect.getWidth()), 0,
                (leftRect.getWidth() + middleRect.getWidth() + sigRect.getWidth() / 4),
                sigRect.getHeight() - Constant.MARGIN);
        ColumnText ct31 = new ColumnText(t);
        ct31.setSimpleColumn(rightRectNames);

        List<Paragraph> paragraphsNames = textFields.stream()
                .map(e -> createParagraph(e.getName(), font, Constant.PARAGRAPH_LEADING, 0))
                .collect(Collectors.toList());
        paragraphsNames.forEach(ct31::addElement);
        ct31.go();

        // Right rectangle - Values
        Rectangle rightRectValues = new Rectangle(
                (Constant.MARGIN * 4 + leftRect.getWidth() + middleRect.getWidth() + rightRectNames.getWidth()), 0,
                sigRect.getWidth(), (sigRect.getHeight() - Constant.MARGIN));
        ColumnText ct32 = new ColumnText(t);
        ct32.setSimpleColumn(rightRectValues);

        List<Paragraph> paragraphsValues = textFields.stream()
                .map(e -> createParagraph(e.getValue(), font, Constant.PARAGRAPH_LEADING, 0))
                .collect(Collectors.toList());
        paragraphsValues.forEach(ct32::addElement);
        ct32.go();
        // --------------------------- Custom signature appearance ---------------------
    }

    return appearance;
}



//this is used to first create the empty fields
protected static PdfPCell createSignatureFieldCell(PdfWriter writer, String name, int height) {
    PdfPCell cell = new PdfPCell();
    cell.setMinimumHeight(height);
    cell.setBackgroundColor(BaseColor.WHITE);
    PdfFormField field = PdfFormField.createSignature(writer);
    field.setFieldName(name);
    field.setFlags(PdfAnnotation.FLAGS_PRINT);
    cell.setCellEvent(new MySignatureFieldEvent(field, null, 0));
    return cell;
}

//this is used to try to add the extra empty field to signed document
protected static PdfPCell createSignatureFieldCell(PdfStamper stamper, String name, int height, int pageNumber) {
    PdfPCell cell = new PdfPCell();
    cell.setMinimumHeight(height);
    cell.setBackgroundColor(BaseColor.WHITE);
    
    
    PdfFormField field = PdfFormField.createSignature(stamper.getWriter());
    field.setFieldName(name);
    field.setFlags(PdfAnnotation.FLAGS_PRINT);
    cell.setCellEvent(new MySignatureFieldEvent(field, stamper, pageNumber));
    return cell;
}


public static class MySignatureFieldEvent implements PdfPCellEvent {

    public PdfFormField field;
    public PdfStamper stamper;
    public int pageField;

    public MySignatureFieldEvent(PdfFormField field, PdfStamper stamper, int pageField) {
        this.field = field;
        this.stamper = stamper;
        this.pageField = pageField;
    }

    public void cellLayout(PdfPCell cell, Rectangle position, PdfContentByte[] canvases) {
        PdfWriter writer = canvases[0].getPdfWriter();
        field.setPage();
        field.setWidget(position, PdfAnnotation.HIGHLIGHT_INVERT);

        if (stamper == null) {
            writer.addAnnotation(field);
        }else {
            stamper.addAnnotation(field, pageField);
        }
    }

}

【问题讨论】:

  • 根据您阅读 pdf 规范的方式,不允许向经过认证的 pdf 添加新的签名字段。
  • 我用过PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS,所以我认为这个级别允许添加签名字段对吗?但我的问题是,我应该如何将 pdf 变红并添加字段而不破坏它?

标签: java itext digital-signature


【解决方案1】:

首先,Adobe 最初将认证级别解释为this answer 中所述。特别是,如果文档有证书签名,则禁止添加新字段,即使是签名字段。

这种严格性似乎在此过程中已经丢失,但可能会在更新后的任何时候再次应用,特别是在 pdf-insecurity.org 上最近发布的certification attacks 利用这个宽松的选项之后。

话虽如此,但您永远不能做的是更改静态页面内容!但是,在您的代码中,您可以通过向文档添加一个表(带有事件)来添加额外的签名字段。此表将更改静态页面内容。

因此,尝试只添加一个新的签名字段。

【讨论】:

  • 添加表格并关联字段是在签名之前使其可见的方法,我将尝试仅添加字段。看了漏洞后,意思是说我们只需要使用认证P1?从论文中:“例如,实现了 P1 级别,并且任何后续更改都会受到无效认证的惩罚,而 P2 和 P3 之间没有区别,并且从 P2 开始将注释分类为允许。从攻击者的角度来看,这意味着对于这 11 个应用程序,攻击类 EAA 和 SSA 可以在较低的权限级别下执行。”
  • "添加表格并关联字段是使其在签名前可见的方法" - 只要 PDF 中没有事先签名,您就可以创建签名任何你想要的字段,特别是与静态表格元素结合使用。一旦 PDF 中有(填写)签名,您就受到限制,不得更改静态 PDF 内容。
  • "看了漏洞后,就意味着我们只需要使用认证P1?" - 不,您的报价前面是“对于PDF Architect和Soda PDF,我们有看到了权限的部分实现。” IE。您的报价仅代表这两个 PDF 查看器的(不正确!)行为。您不应试图利用少数观众的弱点,而应按照规范工作。
猜你喜欢
  • 1970-01-01
  • 2015-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-26
相关资源
最近更新 更多