diff --git a/pdf/core/primitives.go b/pdf/core/primitives.go index 571c0d5b..2ffdeeab 100644 --- a/pdf/core/primitives.go +++ b/pdf/core/primitives.go @@ -293,6 +293,9 @@ func (str *PdfObjectString) Str() string { // UTF-16BE is applied when the first two bytes are 0xFE, 0XFF, otherwise decoding of // PDFDocEncoding is performed. func (str *PdfObjectString) Decoded() string { + if str == nil { + return "" + } b := []byte(str.val) if len(b) >= 2 && b[0] == 0xFE && b[1] == 0xFF { // UTF16BE. diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 32e5e894..dbe8e4e8 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -18,7 +18,7 @@ import ( "github.com/unidoc/unidoc/pdf/core" ) -// PdfAppender appends a new Pdf content to an existing Pdf document. +// PdfAppender appends new PDF content to an existing PDF document via incremental updates. type PdfAppender struct { rs io.ReadSeeker parser *core.PdfParser @@ -30,8 +30,11 @@ type PdfAppender struct { xrefs core.XrefTable greatestObjNum int + // List of new objects and a map for quick lookups. newObjects []core.PdfObject hasNewObject map[core.PdfObject]struct{} + + written bool } func getPageResources(p *PdfPage) map[core.PdfObjectName]core.PdfObject { @@ -94,18 +97,24 @@ func getPageResources(p *PdfPage) map[core.PdfObjectName]core.PdfObject { // NewPdfAppender creates a new Pdf appender from a Pdf reader. func NewPdfAppender(reader *PdfReader) (*PdfAppender, error) { - a := &PdfAppender{} - a.rs = reader.rs - a.Reader = reader - a.parser = a.Reader.parser + a := &PdfAppender{ + rs: reader.rs, + Reader: reader, + parser: reader.parser, + } if _, err := a.rs.Seek(0, io.SeekStart); err != nil { return nil, err } var err error - // Create a readonly (immutable) reader. It increases memory using. - // Why? We can not check the original reader objects are changed or not. - // When we merge, replace a page content. The new page will contain objects from the readonly reader and other objects. - // The readonly objects won't append to the result Pdf file. This check is not resource demanding. It checks indirect objects owners only. + + // Create a readonly (immutable) reader. It increases memory use but is necessary to be able + // to detect changes in the original reader objects. + // + // In the case where an existing page is modified, the page contents are replaced upon merging + // (appending). The new page will refer to objects from the read-only reader and new instances + // of objects that have been changes. Objects from the original reader are not appended, only + // new objects that modify the PDF. The change detection check is not resource demanding. It + // only checks owners (source) of indirect objects. a.roReader, err = NewPdfReader(a.rs) if err != nil { return nil, err @@ -131,8 +140,8 @@ func (a *PdfAppender) addNewObjects(obj core.PdfObject) { } switch v := obj.(type) { case *core.PdfIndirectObject: - // Check the object is changing. - // If the indirect object has not the readonly parser then the object is changed. + // If the current parser is different from the read-only parser, then + // the object has changed. if v.GetParser() != a.roReader.parser { a.newObjects = append(a.newObjects, obj) a.hasNewObject[obj] = struct{}{} @@ -147,22 +156,25 @@ func (a *PdfAppender) addNewObjects(obj core.PdfObject) { a.addNewObjects(v.Get(key)) } case *core.PdfObjectStreams: - // Check the object is changing. - // If the indirect object has not the readonly parser then the object is changed. + // If the current parser is different from the read-only parser, then + // the object has changed. if v.GetParser() != a.roReader.parser { for _, o := range v.Elements() { a.addNewObjects(o) } } case *core.PdfObjectStream: - // Check the object is changing. - // If the indirect object has the readonly parser then the object is not changed. - if v.GetParser() == a.roReader.parser { + // If the current parser is different from the read-only parser, then + // the object has changed. + parser := v.GetParser() + if parser == a.roReader.parser { return } - // If the indirect object has not the origin parser then the object may be changed orr not. - if v.GetParser() == a.Reader.parser { - // Check data is not changed. + + // If the current parser is different from the parser of the reader, + // then the object may have changed. + if parser == a.Reader.parser { + // Check if data has changed. if streamObj, err := a.roReader.parser.LookupByReference(v.PdfObjectReference); err == nil { var isNotChanged bool if stream, ok := core.GetStream(streamObj); ok && bytes.Equal(stream.Stream, v.Stream) { @@ -331,7 +343,7 @@ func (a *PdfAppender) MergePageWith(pageNum int, page *PdfPage) error { return nil } -// AddPages adds pages to end of the source Pdf. +// AddPages adds pages to be appended to the end of the source PDF. func (a *PdfAppender) AddPages(pages ...*PdfPage) { for _, page := range pages { page = page.Duplicate() @@ -370,19 +382,74 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } } -// ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which replaces the original acrobat form. +// Sign signs a specific page with a digital signature. +// The signature field parameter must have a valid signature dictionary +// specified by its V field. +func (a *PdfAppender) Sign(pageNum int, field *PdfFieldSignature) error { + if field == nil { + return errors.New("signature field cannot be nil") + } + + signature := field.V + if signature == nil { + return errors.New("signature dictionary cannot be nil") + } + + // Get a copy of the selected page. + pageIndex := pageNum - 1 + if pageIndex < 0 || pageIndex > len(a.pages)-1 { + return fmt.Errorf("page %d not found", pageNum) + } + page := a.pages[pageIndex].Duplicate() + + // Initialize signature. + if err := signature.Initialize(); err != nil { + return err + } + a.addNewObjects(signature.container) + + // Add signature field annotations to the page annotations. + for _, annotation := range field.Annotations { + annotation.P = page.ToPdfObject() + page.Annotations = append(page.Annotations, annotation.PdfAnnotation) + } + + // Add signature field to the form. + acroForm := a.Reader.AcroForm + if acroForm == nil { + acroForm = NewPdfAcroForm() + } + + acroForm.SigFlags = core.MakeInteger(3) + acroForm.DA = core.MakeString("/F1 0 Tf 0 g") + n2ResourcesFont := core.MakeDict() + n2ResourcesFont.Set("F1", DefaultFont().ToPdfObject()) + acroForm.DR = NewPdfPageResources() + acroForm.DR.Font = n2ResourcesFont + + fields := append(acroForm.AllFields(), field.PdfField) + + acroForm.Fields = &fields + a.ReplaceAcroForm(acroForm) + + // Replace original page. + procPage(page) + a.pages[pageIndex] = page + + return nil +} + +// ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which +// replaces the original AcroForm. func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { a.acroForm = acroForm } // Write writes the Appender output to io.Writer. +// It can only be called once and further invocations will result in an error. func (a *PdfAppender) Write(w io.Writer) error { - if _, err := a.rs.Seek(0, io.SeekStart); err != nil { - return err - } - offset, err := io.Copy(w, a.rs) - if err != nil { - return err + if a.written { + return errors.New("appender write can only be invoked once") } writer := NewPdfWriter() @@ -446,7 +513,7 @@ func (a *PdfAppender) Write(w io.Writer) error { common.Log.Trace("Page Parent: %T", parent) parentDict, ok := parent.PdfObject.(*core.PdfObjectDictionary) if !ok { - return errors.New("Invalid Parent object") + return errors.New("invalid Parent object") } for _, field := range inheritedFields { common.Log.Trace("Field %s", field) @@ -473,6 +540,56 @@ func (a *PdfAppender) Write(w io.Writer) error { writer.SetForms(a.acroForm) } + if _, err := a.rs.Seek(0, io.SeekStart); err != nil { + return err + } + + // Digital signature handling: Check if any of the new objects represent a signature dictionary. + // The byte range is later updated dynamically based on the position of the actual signature + // Contents. + digestWriters := make(map[SignatureHandler]io.Writer) + byteRange := core.MakeArray() + for _, obj := range a.newObjects { + if ind, found := core.GetIndirect(obj); found { + if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { + handler := *sigDict.handler + var err error + digestWriters[handler], err = handler.NewDigest(sigDict.signature) + if err != nil { + return err + } + byteRange.Append(core.MakeInteger(0xfffff), core.MakeInteger(0xfffff)) + } + } + } + if byteRange.Len() > 0 { + byteRange.Append(core.MakeInteger(0xfffff), core.MakeInteger(0xfffff)) + } + for _, obj := range a.newObjects { + if ind, found := core.GetIndirect(obj); found { + if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { + sigDict.Set("ByteRange", byteRange) + } + } + } + + hasSigDict := len(digestWriters) > 0 + + var reader io.Reader = a.rs + if hasSigDict { + writers := make([]io.Writer, 0, len(digestWriters)) + for _, hash := range digestWriters { + writers = append(writers, hash) + } + reader = io.TeeReader(a.rs, io.MultiWriter(writers...)) + } + + // Write the original PDF. + offset, err := io.Copy(w, reader) + if err != nil { + return err + } + if len(a.newObjects) == 0 { return nil } @@ -481,14 +598,108 @@ func (a *PdfAppender) Write(w io.Writer) error { writer.ObjNumOffset = a.greatestObjNum writer.appendMode = true writer.appendToXrefs = a.xrefs + writer.minorVersion = 7 for _, obj := range a.newObjects { writer.addObject(obj) } - if err := writer.Write(w); err != nil { + + writerW := w + if hasSigDict { + // For signatures, we need to write twice. First to find the byte offset + // of the Contents and then dynamically update the file with the + // signature and ByteRange. + writerW = bytes.NewBuffer(nil) + } + + // Perform the write. For signatures will do a mock write to a buffer. + if err := writer.Write(writerW); err != nil { return err } - return err + + // TODO(gunnsth): Consider whether the dynamic content can be handled efficiently with generic write hooks? + // Logic is getting pretty complex here. + if hasSigDict { + // Update the byteRanges based on mock write. + bufferData := writerW.(*bytes.Buffer).Bytes() + byteRange := core.MakeArray() + var sigDicts []*pdfSignDictionary + var lastPosition int64 + for _, obj := range writer.objects { + if ind, found := core.GetIndirect(obj); found { + if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { + sigDicts = append(sigDicts, sigDict) + newPosition := sigDict.fileOffset + int64(sigDict.contentsOffsetStart) + byteRange.Append( + core.MakeInteger(lastPosition), + core.MakeInteger(newPosition-lastPosition), + ) + lastPosition = sigDict.fileOffset + int64(sigDict.contentsOffsetEnd) + } + } + } + byteRange.Append( + core.MakeInteger(lastPosition), + core.MakeInteger(offset+int64(len(bufferData))-lastPosition), + ) + // set the ByteRange value + byteRangeData := []byte(byteRange.WriteString()) + for _, sigDict := range sigDicts { + bufferOffset := int(sigDict.fileOffset - offset) + for i := sigDict.byteRangeOffsetStart; i < sigDict.byteRangeOffsetEnd; i++ { + bufferData[bufferOffset+i] = ' ' + } + dst := bufferData[bufferOffset+sigDict.byteRangeOffsetStart : bufferOffset+sigDict.byteRangeOffsetEnd] + copy(dst, byteRangeData) + } + var prevOffset int + for _, sigDict := range sigDicts { + bufferOffset := int(sigDict.fileOffset - offset) + data := bufferData[prevOffset : bufferOffset+sigDict.contentsOffsetStart] + handler := *sigDict.handler + digestWriters[handler].Write(data) + prevOffset = bufferOffset + sigDict.contentsOffsetEnd + } + for _, sigDict := range sigDicts { + data := bufferData[prevOffset:] + handler := *sigDict.handler + digestWriters[handler].Write(data) + } + for _, sigDict := range sigDicts { + bufferOffset := int(sigDict.fileOffset - offset) + handler := *sigDict.handler + digest := digestWriters[handler] + if err := handler.Sign(sigDict.signature, digest); err != nil { + return err + } + contents := []byte(sigDict.signature.Contents.WriteString()) + + // Empty out the ByteRange and Content data. + // FIXME(gunnsth): Is this needed? Seems like the correct data is copied below? Prefer + // to keep the rest space? + for i := sigDict.byteRangeOffsetStart; i < sigDict.byteRangeOffsetEnd; i++ { + bufferData[bufferOffset+i] = ' ' + } + for i := sigDict.contentsOffsetStart; i < sigDict.contentsOffsetEnd; i++ { + bufferData[bufferOffset+i] = ' ' + } + + // Copy the actual ByteRange and Contents data into the buffer prepared by first write. + dst := bufferData[bufferOffset+sigDict.byteRangeOffsetStart : bufferOffset+sigDict.byteRangeOffsetEnd] + copy(dst, byteRangeData) + dst = bufferData[bufferOffset+sigDict.contentsOffsetStart : bufferOffset+sigDict.contentsOffsetEnd] + copy(dst, contents) + } + + buffer := bytes.NewBuffer(bufferData) + _, err = io.Copy(w, buffer) + if err != nil { + return err + } + } + + a.written = true + return nil } // WriteToFile writes the Appender output to file specified by path. diff --git a/pdf/model/appender_test.go b/pdf/model/appender_test.go index a28bfe05..135070d1 100644 --- a/pdf/model/appender_test.go +++ b/pdf/model/appender_test.go @@ -3,20 +3,29 @@ * file 'LICENSE.md', which is part of this source code package. */ -package model +package model_test import ( + "bytes" + "crypto/rsa" + "fmt" + "io/ioutil" "os" "path/filepath" "testing" + "time" + + "golang.org/x/crypto/pkcs12" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" + "github.com/unidoc/unidoc/pdf/model" + "github.com/unidoc/unidoc/pdf/model/sighandler" ) -// This test file contains multiple tests to generate PDFs from existing Pdf files. The outputs are written into /tmp as files. The files -// themselves need to be observed to check for correctness as we don't have a good way to automatically check -// if every detail is correct. +// This test file contains multiple tests to generate PDFs from existing Pdf files. The outputs are written +// into TMPDIR as files. The files themselves need to be observed to check for correctness as we don't have +// a good way to automatically check if every detail is correct. func init() { common.SetLogger(common.NewConsoleLogger(common.LogLevelDebug)) @@ -32,6 +41,11 @@ const imgPdfFile2 = "./testdata/img1-2.pdf" // source http://foersom.com/net/HowTo/data/OoPdfFormExample.pdf const testPdfAcroFormFile1 = "./testdata/OoPdfFormExample.pdf" +const testPdfSignedPDFDocument = "./testdata/SampleSignedPDFDocument.pdf" + +const testPKS12Key = "./testdata/ks12" +const testPKS12KeyPassword = "password" + func tempFile(name string) string { return filepath.Join(os.TempDir(), name) } @@ -43,7 +57,7 @@ func TestAppenderAddPage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -54,13 +68,13 @@ func TestAppenderAddPage(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -85,7 +99,7 @@ func TestAppenderAddPage2(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -96,13 +110,13 @@ func TestAppenderAddPage2(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -126,13 +140,13 @@ func TestAppenderRemovePage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -155,7 +169,7 @@ func TestAppenderReplacePage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -167,13 +181,13 @@ func TestAppenderReplacePage(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -196,21 +210,21 @@ func TestAppenderAddAnnotation(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return } page := pdf1.PageList[0] - annotation := NewPdfAnnotationSquare() - rect := PdfRectangle{Ury: 250.0, Urx: 150.0, Lly: 50.0, Llx: 50.0} + annotation := model.NewPdfAnnotationSquare() + rect := model.PdfRectangle{Ury: 250.0, Urx: 150.0, Lly: 50.0, Llx: 50.0} annotation.Rect = rect.ToPdfObject() annotation.IC = core.MakeArrayFromFloats([]float64{4.0, 0.0, 0.3}) annotation.CA = core.MakeFloat(0.5) @@ -233,7 +247,7 @@ func TestAppenderMergePage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -245,13 +259,13 @@ func TestAppenderMergePage(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -278,7 +292,7 @@ func TestAppenderMergePage2(t *testing.T) { } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -291,13 +305,13 @@ func TestAppenderMergePage2(t *testing.T) { } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -325,7 +339,7 @@ func TestAppenderMergePage3(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -338,13 +352,13 @@ func TestAppenderMergePage3(t *testing.T) { } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -362,3 +376,249 @@ func TestAppenderMergePage3(t *testing.T) { return } } + +func validateFile(t *testing.T, fileName string) { + data, err := ioutil.ReadFile(fileName) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + reader, err := model.NewPdfReader(bytes.NewReader(data)) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, _ := sighandler.NewAdobeX509RSASHA1(nil, nil) + handler2, _ := sighandler.NewAdobePKCS7Detached(nil, nil) + handlers := []model.SignatureHandler{handler, handler2} + + res, err := reader.ValidateSignatures(handlers) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + if len(res) == 0 { + t.Errorf("Fail: signature fields not found") + return + } + + if !res[0].IsSigned || !res[0].IsVerified { + t.Errorf("Fail: validation failed") + return + } + + for i, item := range res { + t.Logf("== Signature %d", i+1) + t.Logf("%s", item.String()) + } +} + +func TestAppenderSignPage4(t *testing.T) { + // TODO move to reader_test.go + validateFile(t, testPdfSignedPDFDocument) + + f1, err := os.Open(testPdfFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f1.Close() + pdf1, err := model.NewPdfReader(f1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appender, err := model.NewPdfAppender(pdf1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + f, _ := ioutil.ReadFile(testPKS12Key) + privateKey, cert, err := pkcs12.Decode(f, testPKS12KeyPassword) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, err := sighandler.NewAdobePKCS7Detached(privateKey.(*rsa.PrivateKey), cert) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + // Create signature field and appearance. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Appender") + signature.SetReason("TestAppenderSignPage4") + signature.SetDate(time.Now(), "") + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Signature1") + + widget := model.NewPdfAnnotationWidget() + widget.F = core.MakeInteger(132) + widget.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + widget.Parent = sigField.GetContainingPdfObject() + + sigField.Annotations = append(sigField.Annotations, widget) + + if err = appender.Sign(1, sigField); err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + err = appender.WriteToFile(tempFile("appender_sign_page_4.pdf")) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + validateFile(t, tempFile("appender_sign_page_4.pdf")) +} + +func TestAppenderSignMultiple(t *testing.T) { + inputPath := testPdfFile1 + + for i := 0; i < 3; i++ { + f, err := os.Open(inputPath) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + pdfReader, err := model.NewPdfReader(f) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + t.Logf("Fields: %d", len(pdfReader.AcroForm.AllFields())) + + if len(pdfReader.AcroForm.AllFields()) != i { + t.Fatalf("fields != %d (got %d)", i, len(pdfReader.AcroForm.AllFields())) + } + + t.Logf("Annotations: %d", len(pdfReader.PageList[0].Annotations)) + if len(pdfReader.PageList[0].Annotations) != i { + t.Fatalf("page annotations != %d (got %d)", i, len(pdfReader.PageList[0].Annotations)) + } + + appender, err := model.NewPdfAppender(pdfReader) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + pfxData, _ := ioutil.ReadFile(testPKS12Key) + privateKey, cert, err := pkcs12.Decode(pfxData, testPKS12KeyPassword) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + handler, err := sighandler.NewAdobePKCS7Detached(privateKey.(*rsa.PrivateKey), cert) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + // Create signature field and appearance. + signature := model.NewPdfSignature(handler) + signature.SetName(fmt.Sprintf("Test Appender - Round %d", i+1)) + signature.SetReason("TestAppenderSignPage4") + signature.SetDate(time.Now(), "") + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Signature1") + + widget := model.NewPdfAnnotationWidget() + widget.F = core.MakeInteger(132) + widget.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + widget.Parent = sigField.GetContainingPdfObject() + + sigField.Annotations = append(sigField.Annotations, widget) + + if err = appender.Sign(1, sigField); err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + outPath := tempFile(fmt.Sprintf("appender_sign_multiple_%d.pdf", i+1)) + + err = appender.WriteToFile(outPath) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + validateFile(t, outPath) + inputPath = outPath + + f.Close() + } +} + +// Each Appender can only be written out once, further invokations of Write should result in an error. +func TestAppenderAttemptMultiWrite(t *testing.T) { + f1, err := os.Open(testPdfLoremIpsumFile) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f1.Close() + pdf1, err := model.NewPdfReader(f1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + f2, err := os.Open(testPdfFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f2.Close() + pdf2, err := model.NewPdfReader(f2) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appender, err := model.NewPdfAppender(pdf1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appender.AddPages(pdf1.PageList...) + appender.AddPages(pdf2.PageList...) + appender.AddPages(pdf2.PageList...) + + // Write twice to buffer and compare results. + var buf1, buf2 bytes.Buffer + err = appender.Write(&buf1) + if err != nil { + t.Fatalf("Error: %v", err) + } + err = appender.Write(&buf2) + if err == nil { + t.Fatalf("Second invokation of appender.Write should yield an error") + } +} diff --git a/pdf/model/fields.go b/pdf/model/fields.go index 5c3bc02f..70b2dac2 100644 --- a/pdf/model/fields.go +++ b/pdf/model/fields.go @@ -245,50 +245,37 @@ func (f *PdfField) String() string { } // ToPdfObject sets the common field elements. -// Note: Call the more field context's ToPdfObject to set both the generic and non-generic information. +// Note: Call the more field context's ToPdfObject to set both the generic and +// non-generic information. func (f *PdfField) ToPdfObject() core.PdfObject { container := f.container d := container.PdfObject.(*core.PdfObjectDictionary) - d.SetIfNotNil("FT", f.FT) - if f.Parent != nil { - d.Set("Parent", f.Parent.GetContainingPdfObject()) + // Create an array of the kids (fields or widgets). + kids := core.MakeArray() + for _, child := range f.Kids { + kids.Append(child.ToPdfObject()) + } + for _, annot := range f.Annotations { + kids.Append(annot.GetContext().ToPdfObject()) } - if f.Kids != nil { - // Create an array of the kids (fields or widgets). - kids := core.MakeArray() - for _, child := range f.Kids { - kids.Append(child.ToPdfObject()) - } + // Set fields. + if f.Parent != nil { + d.SetIfNotNil("Parent", f.Parent.GetContainingPdfObject()) + } + if kids.Len() > 0 { d.Set("Kids", kids) } - if f.Annotations != nil { - _, hasKids := d.Get("Kids").(*core.PdfObjectArray) - if !hasKids { - d.Set("Kids", &core.PdfObjectArray{}) - } - // TODO: If only 1 widget annotation, it can be merged in. - kids := d.Get("Kids").(*core.PdfObjectArray) - for _, annot := range f.Annotations { - kids.Append(annot.GetContext().ToPdfObject()) - } - } - + d.SetIfNotNil("FT", f.FT) d.SetIfNotNil("T", f.T) d.SetIfNotNil("TU", f.TU) d.SetIfNotNil("TM", f.TM) d.SetIfNotNil("Ff", f.Ff) - if f.V != nil { - d.Set("V", f.V) - } - if f.DV != nil { - d.Set("DV", f.DV) - } - if f.AA != nil { - d.Set("AA", f.AA) - } + d.SetIfNotNil("V", f.V) + d.SetIfNotNil("DV", f.DV) + d.SetIfNotNil("AA", f.AA) return container } @@ -446,28 +433,38 @@ func (ch *PdfFieldChoice) ToPdfObject() core.PdfObject { // the name of the signer and verifying document contents. type PdfFieldSignature struct { *PdfField + V *PdfSignature Lock *core.PdfIndirectObject SV *core.PdfIndirectObject } +// NewPdfFieldSignature returns an initialized signature field. +func NewPdfFieldSignature(signature *PdfSignature) *PdfFieldSignature { + field := &PdfFieldSignature{ + PdfField: NewPdfField(), + V: signature, + } + field.PdfField.SetContext(field) + field.FT = core.MakeName("Sig") + + return field +} + // ToPdfObject returns an indirect object containing the signature field dictionary. func (sig *PdfFieldSignature) ToPdfObject() core.PdfObject { // Set general field attributes sig.PdfField.ToPdfObject() - container := sig.container // Handle signature field specific attributes + container := sig.container + d := container.PdfObject.(*core.PdfObjectDictionary) - d.Set("FT", core.MakeName("Sig")) + d.SetIfNotNil("FT", core.MakeName("Sig")) + d.SetIfNotNil("Lock", sig.Lock) + d.SetIfNotNil("SV", sig.SV) if sig.V != nil { - d.Set("V", sig.V.ToPdfObject()) - } - if sig.Lock != nil { - d.Set("Lock", sig.Lock) - } - if sig.SV != nil { - d.Set("SV", sig.SV) + d.SetIfNotNil("V", sig.V.ToPdfObject()) } return container diff --git a/pdf/model/form.go b/pdf/model/form.go index b965318b..818a43a4 100644 --- a/pdf/model/form.go +++ b/pdf/model/form.go @@ -53,6 +53,9 @@ func flattenFields(field *PdfField) []*PdfField { // AllFields returns a flattened list of all fields in the form. func (form *PdfAcroForm) AllFields() []*PdfField { + if form == nil { + return nil + } var fields []*PdfField if form.Fields != nil { for _, field := range *form.Fields { diff --git a/pdf/model/sighandler/doc.go b/pdf/model/sighandler/doc.go new file mode 100644 index 00000000..8aabfc10 --- /dev/null +++ b/pdf/model/sighandler/doc.go @@ -0,0 +1,7 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +// Package sighandler implements digital signature handlers for PDF signature validation and signing. +package sighandler diff --git a/pdf/model/sighandler/sighandler_pkcs7.go b/pdf/model/sighandler/sighandler_pkcs7.go new file mode 100644 index 00000000..5409e57a --- /dev/null +++ b/pdf/model/sighandler/sighandler_pkcs7.go @@ -0,0 +1,128 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package sighandler + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "errors" + + "github.com/gunnsth/pkcs7" + + "github.com/unidoc/unidoc/pdf/core" + "github.com/unidoc/unidoc/pdf/model" +) + +// Adobe PKCS7 detached signature handler. +type adobePKCS7Detached struct { + privateKey *rsa.PrivateKey + certificate *x509.Certificate +} + +// NewAdobePKCS7Detached creates a new Adobe.PPKMS/Adobe.PPKLite adbe.pkcs7.detached signature handler. +// The both parameters may be nil for the signature validation. +func NewAdobePKCS7Detached(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (model.SignatureHandler, error) { + return &adobePKCS7Detached{certificate: certificate, privateKey: privateKey}, nil +} + +// InitSignature initialises the PdfSignature. +func (a *adobePKCS7Detached) InitSignature(sig *model.PdfSignature) error { + if a.certificate == nil { + return errors.New("certificate must not be nil") + } + if a.privateKey == nil { + return errors.New("privateKey must not be nil") + } + + handler := *a + sig.Handler = &handler + sig.Filter = core.MakeName("Adobe.PPKLite") + sig.SubFilter = core.MakeName("adbe.pkcs7.detached") + sig.Reference = nil + + digest, err := handler.NewDigest(sig) + if err != nil { + return err + } + digest.Write([]byte("calculate the Contents field size")) + return handler.Sign(sig, digest) +} + +func (a *adobePKCS7Detached) getCertificate(sig *model.PdfSignature) (*x509.Certificate, error) { + certificate := a.certificate + if certificate == nil { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return nil, err + } + certificate = certs[0] + } + return certificate, nil +} + +// NewDigest creates a new digest. +func (a *adobePKCS7Detached) NewDigest(sig *model.PdfSignature) (model.Hasher, error) { + return bytes.NewBuffer(nil), nil +} + +// Validate validates PdfSignature. +func (a *adobePKCS7Detached) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) { + signed := sig.Contents.Bytes() + p7, err := pkcs7.Parse(signed) + if err != nil { + return model.SignatureValidationResult{}, err + } + + buffer := digest.(*bytes.Buffer) + p7.Content = buffer.Bytes() + if err = p7.Verify(); err != nil { + return model.SignatureValidationResult{}, err + } + + return model.SignatureValidationResult{ + IsSigned: true, + IsVerified: true, + }, nil +} + +// Sign sets the Contents fields. +func (a *adobePKCS7Detached) Sign(sig *model.PdfSignature, digest model.Hasher) error { + buffer := digest.(*bytes.Buffer) + signedData, err := pkcs7.NewSignedData(buffer.Bytes()) + if err != nil { + return err + } + + // Add the signing cert and private key + if err := signedData.AddSigner(a.certificate, a.privateKey, pkcs7.SignerInfoConfig{}); err != nil { + return err + } + + // Call Detach() is you want to remove content from the signature + // and generate an S/MIME detached signature + signedData.Detach() + // Finish() to obtain the signature bytes + detachedSignature, err := signedData.Finish() + if err != nil { + return err + } + + data := make([]byte, 8192) + copy(data, detachedSignature) + + sig.Contents = core.MakeHexString(string(data)) + return nil +} + +// IsApplicable returns true if the signature handler is applicable for the PdfSignature +func (a *adobePKCS7Detached) IsApplicable(sig *model.PdfSignature) bool { + if sig == nil || sig.Filter == nil || sig.SubFilter == nil { + return false + } + return (*sig.Filter == "Adobe.PPKMS" || *sig.Filter == "Adobe.PPKLite") && *sig.SubFilter == "adbe.pkcs7.detached" +} diff --git a/pdf/model/sighandler/sighandler_rsa_sha1.go b/pdf/model/sighandler/sighandler_rsa_sha1.go new file mode 100644 index 00000000..f0cc4e8c --- /dev/null +++ b/pdf/model/sighandler/sighandler_rsa_sha1.go @@ -0,0 +1,140 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package sighandler + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "errors" + "hash" + + "github.com/unidoc/unidoc/pdf/core" + "github.com/unidoc/unidoc/pdf/model" +) + +// Adobe X509 RSA SHA1 signature handler. +type adobeX509RSASHA1 struct { + privateKey *rsa.PrivateKey + certificate *x509.Certificate +} + +// NewAdobeX509RSASHA1 creates a new Adobe.PPKMS/Adobe.PPKLite adbe.x509.rsa_sha1 signature handler. +// The both parameters may be nil for the signature validation. +func NewAdobeX509RSASHA1(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (model.SignatureHandler, error) { + return &adobeX509RSASHA1{certificate: certificate, privateKey: privateKey}, nil +} + +// InitSignature initialises the PdfSignature. +func (a *adobeX509RSASHA1) InitSignature(sig *model.PdfSignature) error { + if a.certificate == nil { + return errors.New("certificate must not be nil") + } + if a.privateKey == nil { + return errors.New("privateKey must not be nil") + } + + handler := *a + sig.Handler = &handler + sig.Filter = core.MakeName("Adobe.PPKLite") + sig.SubFilter = core.MakeName("adbe.x509.rsa_sha1") + sig.Cert = core.MakeString(string(handler.certificate.Raw)) + sig.Reference = nil + + digest, err := handler.NewDigest(sig) + if err != nil { + return err + } + digest.Write([]byte("calculate the Contents field size")) + return handler.Sign(sig, digest) +} + +func getHashFromSignatureAlgorithm(sa x509.SignatureAlgorithm) (crypto.Hash, bool) { + return crypto.SHA1, true +} + +func (a *adobeX509RSASHA1) getCertificate(sig *model.PdfSignature) (*x509.Certificate, error) { + certificate := a.certificate + if certificate == nil { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return nil, err + } + certificate = certs[0] + } + return certificate, nil +} + +// NewDigest creates a new digest. +func (a *adobeX509RSASHA1) NewDigest(sig *model.PdfSignature) (model.Hasher, error) { + certificate, err := a.getCertificate(sig) + if err != nil { + return nil, err + } + h, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) + return h.New(), nil +} + +// Validate validates PdfSignature. +func (a *adobeX509RSASHA1) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return model.SignatureValidationResult{}, err + } + if len(certs) == 0 { + return model.SignatureValidationResult{}, errors.New("certificate not found") + } + cert := certs[0] + signed := sig.Contents.Bytes() + var sigHash []byte + if _, err := asn1.Unmarshal(signed, &sigHash); err != nil { + return model.SignatureValidationResult{}, err + } + h, ok := digest.(hash.Hash) + if !ok { + return model.SignatureValidationResult{}, errors.New("hash type error") + } + certificate, err := a.getCertificate(sig) + if err != nil { + return model.SignatureValidationResult{}, err + } + ha, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) + if err := rsa.VerifyPKCS1v15(cert.PublicKey.(*rsa.PublicKey), ha, h.Sum(nil), sigHash); err != nil { + return model.SignatureValidationResult{}, err + } + return model.SignatureValidationResult{IsSigned: true, IsVerified: true}, nil +} + +// Sign sets the Contents fields for the PdfSignature. +func (a *adobeX509RSASHA1) Sign(sig *model.PdfSignature, digest model.Hasher) error { + h, ok := digest.(hash.Hash) + if !ok { + return errors.New("hash type error") + } + ha, _ := getHashFromSignatureAlgorithm(a.certificate.SignatureAlgorithm) + data, err := rsa.SignPKCS1v15(rand.Reader, a.privateKey, ha, h.Sum(nil)) + if err != nil { + return err + } + data, err = asn1.Marshal(data) + if err != nil { + return err + } + sig.Contents = core.MakeHexString(string(data)) + return nil +} + +// IsApplicable returns true if the signature handler is applicable for the PdfSignature. +func (a *adobeX509RSASHA1) IsApplicable(sig *model.PdfSignature) bool { + if sig == nil || sig.Filter == nil || sig.SubFilter == nil { + return false + } + return (*sig.Filter == "Adobe.PPKMS" || *sig.Filter == "Adobe.PPKLite") && *sig.SubFilter == "adbe.x509.rsa_sha1" +} diff --git a/pdf/model/signature.go b/pdf/model/signature.go index 55972db5..601a45ff 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -6,14 +6,83 @@ package model import ( + "bytes" + "errors" + "time" + "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" ) +var _ core.PdfObject = &pdfSignDictionary{} + +// pdfSignDictionary is used as a wrapper around PdfSignature for digital checksum calculation +// and population of /Contents and /ByteRange. +// Implements interface core.PdfObject. +type pdfSignDictionary struct { + *core.PdfObjectDictionary + handler *SignatureHandler + signature *PdfSignature + fileOffset int64 + contentsOffsetStart int + contentsOffsetEnd int + byteRangeOffsetStart int + byteRangeOffsetEnd int +} + +// GetSubFilter returns SubFilter value or empty string. +func (d *pdfSignDictionary) GetSubFilter() string { + obj := d.Get("SubFilter") + if obj == nil { + return "" + } + if sf, found := core.GetNameVal(obj); found { + return sf + } + return "" +} + +// WriteString outputs the object as it is to be written to file. +func (d *pdfSignDictionary) WriteString() string { + d.contentsOffsetStart = 0 + d.contentsOffsetEnd = 0 + d.byteRangeOffsetStart = 0 + d.byteRangeOffsetEnd = 0 + out := bytes.NewBuffer(nil) + out.WriteString("<<") + for _, k := range d.Keys() { + v := d.Get(k) + switch k { + case "ByteRange": + out.WriteString(k.WriteString()) + out.WriteString(" ") + d.byteRangeOffsetStart = out.Len() + out.WriteString(v.WriteString()) + out.WriteString(" ") + d.byteRangeOffsetEnd = out.Len() - 1 + case "Contents": + out.WriteString(k.WriteString()) + out.WriteString(" ") + d.contentsOffsetStart = out.Len() + out.WriteString(v.WriteString()) + out.WriteString(" ") + d.contentsOffsetEnd = out.Len() - 1 + default: + out.WriteString(k.WriteString()) + out.WriteString(" ") + out.WriteString(v.WriteString()) + } + } + out.WriteString(">>") + return out.String() +} + // PdfSignature represents a PDF signature dictionary and is used for signing via form signature fields. // (Section 12.8, Table 252 - Entries in a signature dictionary p. 475 in PDF32000_2008). type PdfSignature struct { + Handler SignatureHandler container *core.PdfIndirectObject + // Type: Sig/DocTimeStamp Type *core.PdfObjectName Filter *core.PdfObjectName @@ -35,14 +104,21 @@ type PdfSignature struct { PropAuthType *core.PdfObjectName } -// PdfSignatureReference represents a signature reference dictionary. -// (Table 253 - p. 477 in PDF32000_2008). -type PdfSignatureReference struct { - // Type: SigRef - TransformMethod *core.PdfObjectName - TransformParams *core.PdfObjectDictionary - Data core.PdfObject - DigestMethod *core.PdfObjectName +// NewPdfSignature creates a new PdfSignature object. +func NewPdfSignature(handler SignatureHandler) *PdfSignature { + sig := &PdfSignature{ + Type: core.MakeName("Sig"), + Handler: handler, + } + + dict := &pdfSignDictionary{ + PdfObjectDictionary: core.MakeDict(), + handler: &handler, + signature: sig, + } + + sig.container = core.MakeIndirectObject(dict) + return sig } // GetContainingPdfObject implements interface PdfModel. @@ -50,48 +126,68 @@ func (sig *PdfSignature) GetContainingPdfObject() core.PdfObject { return sig.container } +// SetName sets the `Name` field of the signature. +func (sig *PdfSignature) SetName(name string) { + sig.Name = core.MakeString(name) +} + +// SetDate sets the `M` field of the signature. +func (sig *PdfSignature) SetDate(date time.Time, format string) { + if format == "" { + format = "D:20060102150405-07'00'" + } + + sig.M = core.MakeString(date.Format(format)) +} + +// SetReason sets the `Reason` field of the signature. +func (sig *PdfSignature) SetReason(reason string) { + sig.Reason = core.MakeString(reason) +} + +// SetLocation sets the `Location` field of the signature. +func (sig *PdfSignature) SetLocation(location string) { + sig.Location = core.MakeString(location) +} + +// Initialize initializes the PdfSignature. +func (sig *PdfSignature) Initialize() error { + if sig.Handler == nil { + return errors.New("signature handler cannot be nil") + } + + return sig.Handler.InitSignature(sig) +} + // ToPdfObject implements interface PdfModel. func (sig *PdfSignature) ToPdfObject() core.PdfObject { container := sig.container - dict := container.PdfObject.(*core.PdfObjectDictionary) + + var dict *core.PdfObjectDictionary + if sigDict, ok := container.PdfObject.(*pdfSignDictionary); ok { + dict = sigDict.PdfObjectDictionary + } else { + dict = container.PdfObject.(*core.PdfObjectDictionary) + } dict.Set("Type", sig.Type) + dict.SetIfNotNil("Filter", sig.Filter) + dict.SetIfNotNil("SubFilter", sig.SubFilter) + dict.SetIfNotNil("Contents", sig.Contents) + dict.SetIfNotNil("Cert", sig.Cert) + dict.SetIfNotNil("ByteRange", sig.ByteRange) + dict.SetIfNotNil("Reference", sig.Reference) + dict.SetIfNotNil("Changes", sig.Changes) + dict.SetIfNotNil("Name", sig.Name) + dict.SetIfNotNil("M", sig.M) + dict.SetIfNotNil("Reason", sig.Reason) + dict.SetIfNotNil("ContactInfo", sig.ContactInfo) + dict.SetIfNotNil("ByteRange", sig.ByteRange) + dict.SetIfNotNil("Contents", sig.Contents) - if sig.Filter != nil { - dict.Set("Filter", sig.Filter) - } - if sig.SubFilter != nil { - dict.Set("SubFilter", sig.SubFilter) - } - if sig.Contents != nil { - dict.Set("Contents", sig.Contents) - } - if sig.Cert != nil { - dict.Set("Cert", sig.Cert) - } - if sig.ByteRange != nil { - dict.Set("ByteRange", sig.ByteRange) - } - if sig.Reference != nil { - dict.Set("Reference", sig.Reference) - } - if sig.Changes != nil { - dict.Set("Changes", sig.Changes) - } - if sig.Name != nil { - dict.Set("Name", sig.Name) - } - if sig.M != nil { - dict.Set("M", sig.M) - } - if sig.Reason != nil { - dict.Set("Reason", sig.Reason) - } - if sig.ContactInfo != nil { - dict.Set("ContactInfo", sig.ContactInfo) - } - - // FIXME: ByteRange and Contents need to be updated dynamically. + // NOTE: ByteRange and Contents need to be updated dynamically. + // TODO: Currently dynamic update is only in the appender, need to support + // in the PdfWriter too for the initial signature on document creation. return container } @@ -148,106 +244,3 @@ func (r *PdfReader) newPdfSignatureFromIndirect(container *core.PdfIndirectObjec return sig, nil } - -// PdfSignatureField represents a form field that contains a digital signature. -// (12.7.4.5 - Signature Fields p. 454 in PDF32000_2008). -// -// The signature form field serves two primary purposes. 1. Define the form field that will provide the -// visual signing properties for display but may also hold information needed when the actual signing -// takes place such as signature method. This carries information from the author of the document to the -// software that later does signing. -// -// Filling in (signing) the signature field entails updating at least the V entry and usually the AP entry of the -// associated widget annotation. (Exporting a signature field exports the T, V, AP entries) -// -// The annotation rectangle (Rect) in such a dictionary shall give the position of the field on its page. Signature -// fields that are not intended to be visible shall have an annotation rectangle that has zero height and width. PDF -// processors shall treat such signatures as not visible. PDF processors shall also treat signatures as not -// visible if either the Hidden bit or the NoView bit of the F entry is true -// -// The location of a signature within a document can have a bearing on its legal meaning. For this reason, -// signature fields shall never refer to more than one annotation. -type PdfSignatureField struct { - container *core.PdfIndirectObject - - V *PdfSignature - // Type: /Sig - // V: *PdfSignature... - Lock *core.PdfIndirectObject // Shall be an indirect reference. - SV *core.PdfIndirectObject // Shall be an indirect reference. -} - -// NewPdfSignatureField prepares a PdfSignatureField from a PdfSignature. -func NewPdfSignatureField(signature *PdfSignature) *PdfSignatureField { - return &PdfSignatureField{ - V: signature, - container: core.MakeIndirectObject(core.MakeDict()), - } -} - -// ToPdfObject implements interface PdfModel. -func (sf *PdfSignatureField) ToPdfObject() core.PdfObject { - container := sf.container - dict := container.PdfObject.(*core.PdfObjectDictionary) - - dict.Set("FT", core.MakeName("Sig")) - - if sf.V != nil { - dict.Set("V", sf.V.ToPdfObject()) - } - if sf.Lock != nil { - dict.Set("Lock", sf.Lock) - } - if sf.SV != nil { - dict.Set("SV", sf.SV) - } - // Other standard fields... - - return container -} - -// PdfSignatureFieldLock represents signature field lock dictionary. -// (Table 233 - p. 455 in PDF32000_2008). -type PdfSignatureFieldLock struct { - Type core.PdfObject - Action *core.PdfObjectName - Fields *core.PdfObjectArray - P *core.PdfObjectInteger -} - -// PdfSignatureFieldSeed represents signature field seed value dictionary. -// (Table 234 - p. 455 in PDF32000_2008). -type PdfSignatureFieldSeed struct { - // Type - Ff *core.PdfObjectInteger - Filter *core.PdfObjectName - SubFilter *core.PdfObjectArray - DigestMethod *core.PdfObjectArray - V *core.PdfObjectInteger - Cert core.PdfObject - Reasons *core.PdfObjectArray - MDP *core.PdfObjectDictionary - TimeStamp *core.PdfObjectDictionary - LegalAttestation *core.PdfObjectArray - AddRevInfo *core.PdfObjectBool - LockDocument *core.PdfObjectName - AppearanceFilter *core.PdfObjectString -} - -// PdfCertificateSeed represents certificate seed value dictionary. -// (Table 235 - p. 457 in PDF32000_2008). -type PdfCertificateSeed struct { - // Type - Ff *core.PdfObjectInteger - Subject *core.PdfObjectArray - SignaturePolicyOID *core.PdfObjectString - SignaturePolicyHashValue *core.PdfObjectString - SignaturePolicyHashAlgorithm *core.PdfObjectName - SignaturePolicyCommitmentType *core.PdfObjectArray - SubjectDN *core.PdfObjectArray - KeyUsage *core.PdfObjectArray - Issuer *core.PdfObjectArray - OID *core.PdfObjectArray - URL *core.PdfObjectString - URLType *core.PdfObjectName -} diff --git a/pdf/model/signature_handler.go b/pdf/model/signature_handler.go new file mode 100644 index 00000000..a1a2f286 --- /dev/null +++ b/pdf/model/signature_handler.go @@ -0,0 +1,204 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "bytes" + "fmt" + "io" + + "github.com/unidoc/unidoc/common" + "github.com/unidoc/unidoc/pdf/core" +) + +// Hasher is the interface that wraps the basic Write method. +type Hasher interface { + Write(p []byte) (n int, err error) +} + +// SignatureHandler interface defines the common functionality for PDF signature handlers, which +// need to be capable of validating digital signatures and signing PDF documents. +type SignatureHandler interface { + IsApplicable(sig *PdfSignature) bool + Validate(sig *PdfSignature, digest Hasher) (SignatureValidationResult, error) + // InitSignature sets the PdfSignature parameters. + InitSignature(*PdfSignature) error + NewDigest(sig *PdfSignature) (Hasher, error) + Sign(sig *PdfSignature, digest Hasher) error +} + +// SignatureValidationResult defines the response from the signature validation handler. +type SignatureValidationResult struct { + // List of errors when validating the signature. + Errors []string + IsSigned bool + IsVerified bool + IsTrusted bool + Fields []*PdfField + Name string + Date PdfDate + Reason string + Location string + ContactInfo string + + // TODO(gunnsth): Add more fields such as ability to access the certificate information (name, CN, etc). + // TODO: Also add flags to indicate whether the signature covers the entire file, or the entire portion of + // a revision (if incremental updates used). +} + +func (v SignatureValidationResult) String() string { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("Name: %s\n", v.Name)) + if v.Date.year > 0 { + buf.WriteString(fmt.Sprintf("Date: %s\n", v.Date.ToGoTime().String())) + } else { + buf.WriteString("Date not specified\n") + } + if len(v.Reason) > 0 { + buf.WriteString(fmt.Sprintf("Reason: %s\n", v.Reason)) + } else { + buf.WriteString("No reason specified\n") + } + if len(v.Location) > 0 { + buf.WriteString(fmt.Sprintf("Location: %s\n", v.Location)) + } else { + buf.WriteString("Location not specified\n") + } + if len(v.ContactInfo) > 0 { + buf.WriteString(fmt.Sprintf("Contact Info: %s\n", v.ContactInfo)) + } else { + buf.WriteString("Contact info not specified\n") + } + buf.WriteString(fmt.Sprintf("Fields: %d\n", len(v.Fields))) + if v.IsSigned { + buf.WriteString("Signed: Document is signed\n") + } else { + buf.WriteString("Signed: Not signed\n") + } + if v.IsVerified { + buf.WriteString("Signature validation: Is valid\n") + } else { + buf.WriteString("Signature validation: Is invalid\n") + } + if v.IsTrusted { + buf.WriteString("Trusted: Certificate is trusted\n") + } else { + buf.WriteString("Trusted: Untrusted certificate\n") + } + + return buf.String() +} + +// ValidateSignatures validates digital signatures in the document. +func (r *PdfReader) ValidateSignatures(handlers []SignatureHandler) ([]SignatureValidationResult, error) { + if r.AcroForm == nil { + return nil, nil + } + if r.AcroForm.Fields == nil { + return nil, nil + } + type sigFieldPair struct { + sig *PdfSignature + field *PdfField + handler SignatureHandler + } + + var pairs []*sigFieldPair + for _, f := range r.AcroForm.AllFields() { + if f.V == nil { + continue + } + if d, found := core.GetDict(f.V); found { + if name, ok := core.GetNameVal(d.Get("Type")); ok && name == "Sig" { + ind, found := core.GetIndirect(f.V) + if !found { + common.Log.Debug("ERROR: Signature container is nil") + return nil, ErrTypeCheck + } + + sig, err := r.newPdfSignatureFromIndirect(ind) + if err != nil { + return nil, err + } + + // Search for an appropriate handler. + var sigHandler SignatureHandler + for _, handler := range handlers { + if handler.IsApplicable(sig) { + sigHandler = handler + break + } + } + + pairs = append(pairs, &sigFieldPair{ + sig: sig, + field: f, + handler: sigHandler, + }) + } + } + } + + var results []SignatureValidationResult + for _, pair := range pairs { + defaultResult := SignatureValidationResult{ + IsSigned: true, + Fields: []*PdfField{pair.field}, + } + if pair.handler == nil { + defaultResult.Errors = append(defaultResult.Errors, "handler not set") + results = append(results, defaultResult) + continue + } + digest, err := pair.handler.NewDigest(pair.sig) + if err != nil { + defaultResult.Errors = append(defaultResult.Errors, "digest error", err.Error()) + results = append(results, defaultResult) + continue + } + byteRange := pair.sig.ByteRange + if byteRange == nil { + defaultResult.Errors = append(defaultResult.Errors, "ByteRange not set") + results = append(results, defaultResult) + continue + } + + for i := 0; i < byteRange.Len(); i = i + 2 { + start, _ := core.GetNumberAsInt64(byteRange.Get(i)) + ln, _ := core.GetIntVal(byteRange.Get(i + 1)) + if _, err := r.rs.Seek(start, io.SeekStart); err != nil { + return nil, err + } + data := make([]byte, ln) + if _, err := r.rs.Read(data); err != nil { + return nil, err + } + digest.Write(data) + } + + result, err := pair.handler.Validate(pair.sig, digest) + if err != nil { + return nil, err + } + + result.Name = pair.sig.Name.Decoded() + result.Reason = pair.sig.Reason.Decoded() + if pair.sig.M != nil { + sigDate, err := NewPdfDate(pair.sig.M.String()) + if err != nil { + result.Errors = append(result.Errors, err.Error()) + continue + } + result.Date = sigDate + } + result.ContactInfo = pair.sig.ContactInfo.Decoded() + result.Location = pair.sig.Location.Decoded() + + result.Fields = defaultResult.Fields + results = append(results, result) + } + return results, nil +} diff --git a/pdf/model/structures.go b/pdf/model/structures.go index f8b39902..47a057b1 100644 --- a/pdf/model/structures.go +++ b/pdf/model/structures.go @@ -14,6 +14,7 @@ import ( "math" "regexp" "strconv" + "time" "github.com/unidoc/unidoc/pdf/core" ) @@ -93,6 +94,21 @@ type PdfDate struct { utOffsetMins int64 // mm (00-59) } +// ToGoTime returns the date in time.Time format. +func (d PdfDate) ToGoTime() time.Time { + utcOffset := int(d.utOffsetHours*60*60 + d.utOffsetMins*60) + switch d.utOffsetSign { + case '-': + utcOffset = -utcOffset + case 'Z': + utcOffset = 0 + } + tzName := fmt.Sprintf("UTC%c%.2d%.2d", d.utOffsetSign, d.utOffsetHours, d.utOffsetMins) + tz := time.FixedZone(tzName, utcOffset) + + return time.Date(int(d.year), time.Month(d.month), int(d.day), int(d.hour), int(d.minute), int(d.second), 0, tz) +} + var reDate = regexp.MustCompile(`\s*D\s*:\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([+-Z])?(\d{2})?'?(\d{2})?`) // NewPdfDate returns a new PdfDate object from a PDF date string (see 7.9.4 Dates). diff --git a/pdf/model/testdata/SampleSignedPDFDocument.pdf b/pdf/model/testdata/SampleSignedPDFDocument.pdf new file mode 100644 index 00000000..8858f933 Binary files /dev/null and b/pdf/model/testdata/SampleSignedPDFDocument.pdf differ diff --git a/pdf/model/testdata/ks12 b/pdf/model/testdata/ks12 new file mode 100644 index 00000000..b8a13ce6 Binary files /dev/null and b/pdf/model/testdata/ks12 differ diff --git a/pdf/model/writer.go b/pdf/model/writer.go index 0c34a035..7e4b653b 100644 --- a/pdf/model/writer.go +++ b/pdf/model/writer.go @@ -225,6 +225,18 @@ func copyObject(obj core.PdfObject, objectToObjectCopyMap map[core.PdfObject]cor newObj := core.PdfObjectBool(*t) objectToObjectCopyMap[obj] = &newObj return &newObj + case *pdfSignDictionary: + newObj := &pdfSignDictionary{ + PdfObjectDictionary: core.MakeDict(), + handler: t.handler, + signature: t.signature, + } + objectToObjectCopyMap[obj] = newObj + for _, key := range t.Keys() { + val := t.Get(key) + newObj.Set(key, copyObject(val, objectToObjectCopyMap)) + } + return newObj default: common.Log.Info("TODO(a5i): implement copyObject for %+v", obj) } @@ -571,6 +583,9 @@ func (w *PdfWriter) writeObject(num int, obj core.PdfObject) { if pobj, isIndirect := obj.(*core.PdfIndirectObject); isIndirect { w.crossReferenceMap[num] = crossReference{Type: 1, Offset: w.writePos, Generation: pobj.GenerationNumber} outStr := fmt.Sprintf("%d 0 obj\n", num) + if sDict, ok := pobj.PdfObject.(*pdfSignDictionary); ok { + sDict.fileOffset = w.writePos + int64(len(outStr)) + } outStr += pobj.PdfObject.WriteString() outStr += "\nendobj\n" w.writeString(outStr)