unidoc / unipdf

Golang PDF library for creating and processing PDF files (pure go)
https://unidoc.io
Other
2.47k stars 250 forks source link

[BUG] Adding signature with image modifies document #503

Closed acha-bill closed 1 year ago

acha-bill commented 1 year ago

Description

When I sign a document that has existing signature, the existing signatures become invalid. Adobe Acrobat says "page modified" Screenshot 2022-12-12 at 18 15 31

Expected Behavior

Existing signatures should still be valid

Version

v3.40.0

Actual Behavior

pdfReader := model.NewPdfReader(content)
pdfAppender, _ := model.NewPdfAppender(pdfReader)

signature := model.NewPdfSignature(sigHandler)
sigFieldOpts := annotator.NewSignatureFieldOpts()
sigFieldOpts.Rect = sigRect
sigFieldOpts.WatermarkImage = sigImage
sigFieldOpts.Encoder = core.NewFlateEncoder()

sigField, _ := annotator.NewSignatureField(signature, nil, sigFieldOpts)
sigField.T = "signature"
pdfAppender.Sign(sigPageNo, sigField)

buffer := bytes.NewBuffer(nil)
if err := pdfAppender.Write(buffer); err != nil {
  return nil, err
}

Attachments

Include a self-contained reproducible code snippet and PDF file that demonstrates the issue. input.pdf

Reproducable repo: https://github.com/acha-bill/unidoc-test

github-actions[bot] commented 1 year ago

Welcome! Thanks for posting your first issue. The way things work here is that while customer issues are prioritized, other issues go into our backlog where they are assessed and fitted into the roadmap when suitable. If you need to get this done, consider buying a license which also enables you to use it in your commercial products. More information can be found on https://unidoc.io/

sampila commented 1 year ago

Hi @acha-bill,

Could you share a runnable code snippet and the PDF file also? This the article about editing the signed PDF https://helpx.adobe.com/acrobat/kb/edit-signed-PDF.html hope this help.

Best regards, Alip

acha-bill commented 1 year ago

@sampila here you go: https://github.com/acha-bill/unidoc-test

package main

import (
    "bytes"
    "crypto/sha512"
    "encoding/hex"
    "fmt"
    "hash"
    "image"
    "io"
    "net/http"
    "os"
    "time"

    "github.com/unidoc/unipdf/v3/annotator"
    "github.com/unidoc/unipdf/v3/core"
    "github.com/unidoc/unipdf/v3/model"
)

func main() {
    if err := sign(); err != nil {
        os.Exit(1)
    }
}

type signatureHandler struct {
    sign func(hash string) ([]byte, error)
}

func newSignatureHandler(sign func(hash string) ([]byte, error)) *signatureHandler {
    return &signatureHandler{
        sign: sign,
    }
}

func (t *signatureHandler) IsApplicable(sig *model.PdfSignature) bool {
    return true
}

func (t *signatureHandler) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) {
    return model.SignatureValidationResult{
        IsSigned:   true,
        IsVerified: true,
    }, nil
}

func (t *signatureHandler) InitSignature(sig *model.PdfSignature) error {
    return nil
}

func (t *signatureHandler) NewDigest(sig *model.PdfSignature) (model.Hasher, error) {
    return sha512.New(), nil
}

func (t *signatureHandler) Sign(sig *model.PdfSignature, hasher model.Hasher) error {
    h := hasher.(hash.Hash)
    digest := hex.EncodeToString(h.Sum(nil))
    signature, err := t.sign(digest)
    if err != nil {
        return err
    }
    data := make([]byte, len(signature))
    copy(data, signature)
    sig.Contents = core.MakeHexString(string(data))
    return nil
}

func sign() error {
    f, err := os.Open("./input.pdf")
    if err != nil {
        return err
    }

    defer f.Close()

    pdfReader, err := model.NewPdfReader(f)
    if err != nil {
        return err
    }
    now := time.Now()
    handler := newSignatureHandler(func(hash string) ([]byte, error) {
        return nil, nil
    })
    signature := model.NewPdfSignature(handler)
    signature.SetDate(now, "")

    // some image online
    c := http.Client{}
    resp, err := c.Get("https://www.freepnglogos.com/uploads/signature-png/signature-download-clip-art-20.png")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    sigImage, _, err := image.Decode(resp.Body)
    if err != nil {
        return err
    }
    imgEncoder := core.NewFlateEncoder()
    sigFieldOpts := annotator.NewSignatureFieldOpts()
    sigFieldOpts.Rect = []float64{10, 25, 110, 75}
    sigFieldOpts.WatermarkImage = sigImage
    sigFieldOpts.Encoder = imgEncoder

    sigField, err := annotator.NewSignatureField(signature, nil, sigFieldOpts)
    if err != nil {
        return err
    }
    sigField.T = core.MakeString("signature")

    pdfAppender, err := model.NewPdfAppender(pdfReader)
    if err != nil {
        return err
    }

    if err = pdfAppender.Sign(2, sigField); err != nil {
        return err
    }

    buffer := bytes.NewBuffer(nil)
    if err = pdfAppender.Write(buffer); err != nil {
        return err
    }

    outputFile, err := os.Create("./output.pdf")
    if err != nil {
        return fmt.Errorf("couldn't create output file: %s", err)
    }
    defer outputFile.Close()

    _, err = io.Copy(outputFile, bytes.NewReader(buffer.Bytes()))
    if err != nil {
        return fmt.Errorf("couldn't copy document buffer to output file: %s", err)
    }

    return nil
}

Here is the input: input.pdf

acha-bill commented 1 year ago

I also noticed that just opening the file with pdfAppender modifies the document.

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"

    "github.com/unidoc/unipdf/v3/model"
)

func main() {
    if err := open(); err != nil {
        os.Exit(1)
    }
}

func open() error {
    f, err := os.Open("./input.pdf")
    if err != nil {
        return err
    }

    defer f.Close()

    pdfReader, err := model.NewPdfReader(f)
    if err != nil {
        return err
    }

    pdfAppender, err := model.NewPdfAppender(pdfReader)
    if err != nil {
        return err
    }

    buffer := bytes.NewBuffer(nil)
    if err = pdfAppender.Write(buffer); err != nil {
        return err
    }

    outputFile, err := os.Create("./output.pdf")
    if err != nil {
        return fmt.Errorf("couldn't create output file: %s", err)
    }
    defer outputFile.Close()

    _, err = io.Copy(outputFile, bytes.NewReader(buffer.Bytes()))
    if err != nil {
        return fmt.Errorf("couldn't copy document buffer to output file: %s", err)
    }

    return nil
}

Here is the input: input.pdf

Screenshot 2022-12-13 at 17 38 04
sampila commented 1 year ago

Thank you for reporting, currently we investigating this issue and trying to fix this.

acha-bill commented 1 year ago

Adding pdfAppender.ReplaceAcroForm(nil) just before pdfAppender.Write fixes the above "miscellaneous change" issue. But the original signature issue remains.

acha-bill commented 1 year ago

Thank you for reporting, currently we investigating this issue and trying to fix this.

Which one? The modified page after signing or the miscellanous change after read/write with appender? :)

sampila commented 1 year ago

We are trying to fixes the signature being invalid when opened on Acrobat after the operation with appender, this should fix the miscellaneous changes with appender also.

acha-bill commented 1 year ago

Thanks. Is there any timeline for this fix?

sampila commented 1 year ago

Hi @acha-bill,

Currently the fixes is under review process.

acha-bill commented 1 year ago

Thanks

acha-bill commented 1 year ago

Hi @sampila Is there any update on this issue or release timeline?

sampila commented 1 year ago

Hi @acha-bill,

The fixes are passed the review and planned to be available on the next version. We will inform you when we released the new version of UniPDF.

stephalba commented 1 year ago

Hi @sampila A key customer project is reaching user acceptance testing in mid-January and we will fail if this bug isn't fixed by then. We need to be at least able to share a timeline for the fix. Do you expect to release the next UniPDF version by January 13th? If not, by when do you expect to release?

sampila commented 1 year ago

Hi @stephalba,

We are planning to release the new UniPDF version at this weekend (7-8 January).

stephalba commented 1 year ago

Hi @sampila That's great news, thanks!

sampila commented 1 year ago

You are welcome @stephalba

sampila commented 1 year ago

Hi @stephalba and @acha-bill,

We released new UniPDF version https://github.com/unidoc/unipdf/releases/tag/v3.42.0, could you try and confirm if this issue solved?

Thanks

stephalba commented 1 year ago

Hi @sampila

Thanks for the update! We will test and get back to you.

acha-bill commented 1 year ago

Hi @sampila

Thanks for the release. Acrobat still says the document has been modified (even though the "Page Modified" flag is no longer shown) See the comparison below

Version 3.38.0

Screenshot 2023-01-09 at 18 24 02

Version 3.42.0

Screenshot 2023-01-09 at 18 22 26

Repo: https://github.com/acha-bill/unidoc-test

sampila commented 1 year ago

Hi @acha-bill,

Are you trying to create signature fields on signed PDF or trying to sign signed PDF?

Here's the code I use, this modified from your repo https://github.com/acha-bill/unidoc-test

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "image"
    "math/big"
    "os"
    "time"

    "github.com/unidoc/unipdf/v3/annotator"
    "github.com/unidoc/unipdf/v3/common/license"
    "github.com/unidoc/unipdf/v3/core"
    "github.com/unidoc/unipdf/v3/model"
    "github.com/unidoc/unipdf/v3/model/sighandler"
)

func LoadUniPDFLicense() error {
    err := license.SetMeteredKey(os.Getenv("UNIDOC_LICENSE_API_KEY"))
    if err != nil {
        return err
    }

    return nil
}

func main() {
    if err := LoadUniPDFLicense(); err != nil {
        os.Exit(1)
    }
    if err := sign(); err != nil {
        os.Exit(1)
    }
}

func sign() error {
    f, err := os.Open("./github_issue_503.pdf")
    if err != nil {
        return err
    }

    defer f.Close()

    pdfReader, err := model.NewPdfReader(f)
    if err != nil {
        return err
    }
    now := time.Now()

    // Generate private key.
    priv, cert, err := generateKeys()
    if err != nil {
        return err
    }

    // Sign input file.
    handler, err := sighandler.NewAdobePKCS7Detached(priv, cert)

    signature := model.NewPdfSignature(handler)
    signature.SetDate(now, "")

    if err := signature.Initialize(); err != nil {
        return err
    }

    // Create the image
    imgFile, err := os.Open("./signature-download-clip-art-20.png")
    if err != nil {
        return err
    }
    defer imgFile.Close()

    sigImage, _, err := image.Decode(imgFile)
    if err != nil {
        return err
    }
    sigFieldOpts := annotator.NewSignatureFieldOpts()
    sigFieldOpts.Rect = []float64{10, 25, 110, 75}
    sigFieldOpts.WatermarkImage = sigImage

    sigField, err := annotator.NewSignatureField(signature, nil, sigFieldOpts)
    if err != nil {
        return err
    }
    sigField.T = core.MakeString("signature")

    pdfAppender, err := model.NewPdfAppender(pdfReader)
    if err != nil {
        return err
    }

    if err = pdfAppender.Sign(2, sigField); err != nil {
        return err
    }

    if err = pdfAppender.WriteToFile("./signed-results.pdf"); err != nil {
        return err
    }

    return nil
}

func generateKeys() (*rsa.PrivateKey, *x509.Certificate, error) {
    now := time.Now()
    // Generate private key.
    priv, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, nil, err
    }

    // Initialize X509 certificate template.
    template := x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject: pkix.Name{
            Organization: []string{"Test Company"},
        },
        NotBefore: now.Add(-time.Hour),
        NotAfter:  now.Add(time.Hour * 24 * 365),

        KeyUsage:              x509.KeyUsageDigitalSignature,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        BasicConstraintsValid: true,
    }

    // Generate X509 certificate.
    certData, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
    if err != nil {
        return nil, nil, err
    }

    cert, err := x509.ParseCertificate(certData)
    if err != nil {
        return nil, nil, err
    }

    return priv, cert, nil
}

Results signed-results.pdf

acha-bill commented 1 year ago

Hi, I'm trying to sign I found out that it works when I use sighandler.NewAdobePKCS7Detached but does not work with a custom handler.

type CustomSignatureHandler struct {
}

func NewCustomSignatureHandler() *CustomSignatureHandler {
    return &CustomSignatureHandler{}
}

func (s *CustomSignatureHandler) IsApplicable(sig *model.PdfSignature) bool {
    return true
}

func (s *CustomSignatureHandler) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) {
    return model.SignatureValidationResult{
        IsSigned:   true,
        IsVerified: true,
    }, nil
}

func (s *CustomSignatureHandler) InitSignature(sig *model.PdfSignature) error {
    return nil
}

func (s *CustomSignatureHandler) NewDigest(sig *model.PdfSignature) (model.Hasher, error) {
    return sha512.New(), nil
}

func (s *CustomSignatureHandler) Sign(sig *model.PdfSignature, hasher model.Hasher) error {
    sig.Contents = core.MakeHexString("test")
    return nil
}

...

handler := NewCustomSignatureHandler()
signature := model.NewPdfSignature(handler)
...

Even if the handler implemention is not correct, I still don't expect the input document to be modified.

sampila commented 1 year ago

Could you try this code and see if the results is correct?

package main

import (
    "bytes"
    "crypto/sha512"
    "errors"
    "image"
    "io/ioutil"
    "log"
    "os"
    "time"

    "github.com/unidoc/unipdf/v3/annotator"
    "github.com/unidoc/unipdf/v3/common"
    "github.com/unidoc/unipdf/v3/common/license"
    "github.com/unidoc/unipdf/v3/core"
    "github.com/unidoc/unipdf/v3/model"
    "github.com/unidoc/unipdf/v3/model/sighandler"
)

func LoadUniPDFLicense() error {
    err := license.SetMeteredKey(os.Getenv("UNIDOC_LICENSE_API_KEY"))
    if err != nil {
        return err
    }
    common.SetLogger(common.NewConsoleLogger(common.LogLevelInfo))

    return nil
}

var now = time.Now()

func main() {
    if err := LoadUniPDFLicense(); err != nil {
        os.Exit(1)
    }
    if err := sign(); err != nil {
        log.Printf("err: %v", err.Error())
        os.Exit(1)
    }
}

func sign() error {
    inputPath := "./github_issue_503.pdf"
    outputPath := "./results-sign-signed.pdf"

    // Generate PDF file signed with empty signature.
    handler, err := sighandler.NewEmptyAdobePKCS7Detached(8192)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    pdfData, signature, err := generateSignedFile(inputPath, handler)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    // Parse signature byte range.
    byteRange, err := parseByteRange(signature.ByteRange)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    // This would be the time to send the PDF buffer to a signing device or
    // signing web service and get back the signature. We will simulate this by
    // signing the PDF using UniDoc and returning the signature data.
    signatureData, err := getExternalSignature(inputPath)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    // Apply external signature to the PDF data buffer.
    // Overwrite the generated empty signature with the signature
    // bytes retrieved from the external service.
    sigBytes := make([]byte, 8192)
    copy(sigBytes, signatureData)

    sig := core.MakeHexString(string(sigBytes)).WriteString()
    copy(pdfData[byteRange[1]:byteRange[2]], []byte(sig))

    // Write output file.
    if err := ioutil.WriteFile(outputPath, pdfData, os.ModePerm); err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    log.Printf("PDF file successfully signed. Output path: %s\n", outputPath)

    return nil
}

func generateSignedFile(inputPath string, handler model.SignatureHandler) ([]byte, *model.PdfSignature, error) {
    // Create reader.
    file, err := os.Open(inputPath)
    if err != nil {
        return nil, nil, err
    }
    defer file.Close()

    reader, err := model.NewPdfReader(file)
    if err != nil {
        return nil, nil, err
    }

    // Create appender.
    appender, err := model.NewPdfAppender(reader)
    if err != nil {
        return nil, nil, err
    }

    signature := model.NewPdfSignature(handler)
    signature.SetName("Signer 2")
    signature.SetDate(now, "")
    if err := signature.Initialize(); err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    // Create the image
    imgFile, err := os.Open("./signature-download-clip-art-20.png")
    if err != nil {
        return nil, nil, err
    }
    defer imgFile.Close()

    sigImage, _, err := image.Decode(imgFile)
    if err != nil {
        return nil, nil, err
    }

    sigFieldOpts := annotator.NewSignatureFieldOpts()
    sigFieldOpts.Rect = []float64{10, 25, 110, 200}
    sigFieldOpts.Image = sigImage

    sigField, err := annotator.NewSignatureField(signature, nil, sigFieldOpts)
    if err != nil {
        return nil, nil, err
    }
    sigField.T = core.MakeString("newSignature")

    if err = appender.Sign(2, sigField); err != nil {
        return nil, nil, err
    }

    // Write PDF file to buffer.
    pdfBuf := bytes.NewBuffer(nil)
    if err = appender.Write(pdfBuf); err != nil {
        return nil, nil, err
    }

    return pdfBuf.Bytes(), signature, nil
}

// getExternalSignature simulates an external service which signs the specified
// PDF file and returns its signature.
func getExternalSignature(inputPath string) ([]byte, error) {
    // Initiate custom signature handler.
    handler := NewCustomSignatureHandler()

    _, signature, err := generateSignedFile(inputPath, handler)
    if err != nil {
        return nil, err
    }

    return signature.Contents.Bytes(), nil
}

// parseByteRange parses the ByteRange value of the signature field.
func parseByteRange(byteRange *core.PdfObjectArray) ([]int64, error) {
    if byteRange == nil {
        return nil, errors.New("byte range cannot be nil")
    }
    if byteRange.Len() != 4 {
        return nil, errors.New("invalid byte range length")
    }

    s1, err := core.GetNumberAsInt64(byteRange.Get(0))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }
    l1, err := core.GetNumberAsInt64(byteRange.Get(1))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }

    s2, err := core.GetNumberAsInt64(byteRange.Get(2))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }
    l2, err := core.GetNumberAsInt64(byteRange.Get(3))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }

    return []int64{s1, s1 + l1, s2, s2 + l2}, nil
}

// Custom Signature handler.
type CustomSignatureHandler struct {
}

func NewCustomSignatureHandler() *CustomSignatureHandler {
    return &CustomSignatureHandler{}
}

func (s *CustomSignatureHandler) IsApplicable(sig *model.PdfSignature) bool {
    return true
}

func (s *CustomSignatureHandler) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) {
    return model.SignatureValidationResult{
        IsSigned:   true,
        IsVerified: true,
    }, nil
}

func (s *CustomSignatureHandler) InitSignature(sig *model.PdfSignature) error {
    return nil
}

func (s *CustomSignatureHandler) NewDigest(sig *model.PdfSignature) (model.Hasher, error) {
    return sha512.New(), nil
}

func (s *CustomSignatureHandler) Sign(sig *model.PdfSignature, hasher model.Hasher) error {
    sig.Contents = core.MakeHexString("test")
    return nil
}

Here's the results I got results-sign-signed.pdf Screenshot 2023-01-10 at 01 29 31

acha-bill commented 1 year ago

I get the same outut as you and it is correct. I'll look for differences between the code above and mine.

Thanks!

sampila commented 1 year ago

You need to use the, which creates a spaces allocation for empty signature.

handler, err := sighandler.NewEmptyAdobePKCS7Detached(8192)

and

// parseByteRange parses the ByteRange value of the signature field.
func parseByteRange(byteRange *core.PdfObjectArray) ([]int64, error) {
    if byteRange == nil {
        return nil, errors.New("byte range cannot be nil")
    }
    if byteRange.Len() != 4 {
        return nil, errors.New("invalid byte range length")
    }

    s1, err := core.GetNumberAsInt64(byteRange.Get(0))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }
    l1, err := core.GetNumberAsInt64(byteRange.Get(1))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }

    s2, err := core.GetNumberAsInt64(byteRange.Get(2))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }
    l2, err := core.GetNumberAsInt64(byteRange.Get(3))
    if err != nil {
        return nil, errors.New("invalid byte range value")
    }

    return []int64{s1, s1 + l1, s2, s2 + l2}, nil
}

Hope it helps.

acha-bill commented 1 year ago

Thanks! 💪🏾

sampila commented 1 year ago

You are welcome, we closing this issue for now, feel free to re-open the issue if this still not resolved.