package sign import ( "crypto" "crypto/x509" "encoding/hex" "fmt" "io" "os" "time" "github.com/digitorus/pdf" "github.com/digitorus/pdfsign/revocation" "github.com/digitorus/pkcs7" "github.com/mattetti/filebuffer" ) type CatalogData struct { ObjectId uint32 RootString string } type TSA struct { URL string Username string Password string } type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error type SignData struct { Signature SignDataSignature Signer crypto.Signer DigestAlgorithm crypto.Hash Certificate *x509.Certificate CertificateChains [][]*x509.Certificate TSA TSA RevocationData revocation.InfoArchival RevocationFunction RevocationFunction Appearance Appearance objectId uint32 } // Appearance represents the appearance of the signature type Appearance struct { Visible bool Page uint32 LowerLeftX float64 LowerLeftY float64 UpperRightX float64 UpperRightY float64 Image []byte // Image data to use as signature appearance ImageAsWatermark bool // If true, the text will be drawn over the image } type VisualSignData struct { pageObjectId uint32 objectId uint32 } type InfoData struct { ObjectId uint32 } //go:generate stringer -type=CertType type CertType uint const ( CertificationSignature CertType = iota + 1 ApprovalSignature UsageRightsSignature TimeStampSignature ) //go:generate stringer -type=DocMDPPerm type DocMDPPerm uint const ( DoNotAllowAnyChangesPerms DocMDPPerm = iota + 1 AllowFillingExistingFormFieldsAndSignaturesPerms AllowFillingExistingFormFieldsAndSignaturesAndCRUDAnnotationsPerms ) type SignDataSignature struct { CertType CertType DocMDPPerm DocMDPPerm Info SignDataSignatureInfo } type SignDataSignatureInfo struct { Name string Location string Reason string ContactInfo string Date time.Time } type SignContext struct { InputFile io.ReadSeeker OutputFile io.Writer OutputBuffer *filebuffer.Buffer SignData SignData CatalogData CatalogData VisualSignData VisualSignData InfoData InfoData PDFReader *pdf.Reader NewXrefStart int64 ByteRangeValues []int64 SignatureMaxLength uint32 SignatureMaxLengthBase uint32 existingSignatures []SignData lastXrefID uint32 newXrefEntries []xrefEntry updatedXrefEntries []xrefEntry } func SignFile(input string, output string, sign_data SignData) error { input_file, err := os.Open(input) if err != nil { return err } defer input_file.Close() output_file, err := os.Create(output) if err != nil { return err } defer output_file.Close() finfo, err := input_file.Stat() if err != nil { return err } size := finfo.Size() rdr, err := pdf.NewReader(input_file, size) if err != nil { return err } return Sign(input_file, output_file, rdr, size, sign_data) } func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, sign_data SignData) error { sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 2 context := SignContext{ PDFReader: rdr, InputFile: input, OutputFile: output, SignData: sign_data, SignatureMaxLengthBase: uint32(hex.EncodedLen(512)), } // Fetch existing signatures existingSignatures, err := context.fetchExistingSignatures() if err != nil { return err } context.existingSignatures = existingSignatures err = context.SignPDF() if err != nil { return err } return nil } func (context *SignContext) SignPDF() error { // set defaults if context.SignData.Signature.CertType == 0 { context.SignData.Signature.CertType = 1 } if context.SignData.Signature.DocMDPPerm == 0 { context.SignData.Signature.DocMDPPerm = 1 } if !context.SignData.DigestAlgorithm.Available() { context.SignData.DigestAlgorithm = crypto.SHA256 } if context.SignData.Appearance.Page == 0 { context.SignData.Appearance.Page = 1 } context.OutputBuffer = filebuffer.New([]byte{}) // Copy old file into new buffer. _, err := context.InputFile.Seek(0, 0) if err != nil { return err } if _, err := io.Copy(context.OutputBuffer, context.InputFile); err != nil { return err } // File always needs an empty line after %%EOF. if _, err := context.OutputBuffer.Write([]byte("\n")); err != nil { return err } // Base size for signature. context.SignatureMaxLength = context.SignatureMaxLengthBase // If not a timestamp signature if context.SignData.Signature.CertType != TimeStampSignature { if context.SignData.Certificate == nil { return fmt.Errorf("certificate is required") } switch context.SignData.Certificate.SignatureAlgorithm.String() { case "SHA1-RSA": case "ECDSA-SHA1": case "DSA-SHA1": context.SignatureMaxLength += uint32(hex.EncodedLen(128)) case "SHA256-RSA": case "ECDSA-SHA256": case "DSA-SHA256": context.SignatureMaxLength += uint32(hex.EncodedLen(256)) case "SHA384-RSA": case "ECDSA-SHA384": context.SignatureMaxLength += uint32(hex.EncodedLen(384)) case "SHA512-RSA": case "ECDSA-SHA512": context.SignatureMaxLength += uint32(hex.EncodedLen(512)) } // Add size of digest algorithm twice (for file digist and signing certificate attribute) context.SignatureMaxLength += uint32(hex.EncodedLen(context.SignData.DigestAlgorithm.Size() * 2)) // Add size for my certificate. degenerated, err := pkcs7.DegenerateCertificate(context.SignData.Certificate.Raw) if err != nil { return fmt.Errorf("failed to degenerate certificate: %w", err) } context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated))) // Add size of the raw issuer which is added by AddSignerChain context.SignatureMaxLength += uint32(hex.EncodedLen(len(context.SignData.Certificate.RawIssuer))) // Add size for certificate chain. var certificate_chain []*x509.Certificate if len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1 { certificate_chain = context.SignData.CertificateChains[0][1:] } if len(certificate_chain) > 0 { for _, cert := range certificate_chain { degenerated, err := pkcs7.DegenerateCertificate(cert.Raw) if err != nil { return fmt.Errorf("failed to degenerate certificate in chain: %w", err) } context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated))) } } // Fetch revocation data before adding signature placeholder. // Revocation data can be quite large and we need to create enough space in the placeholder. if err := context.fetchRevocationData(); err != nil { return fmt.Errorf("failed to fetch revocation data: %w", err) } } // Add estimated size for TSA. // We can't kow actual size of TSA until after signing. // // Different TSA servers provide different response sizes, we // might need to make this configurable or detect and store. if context.SignData.TSA.URL != "" { context.SignatureMaxLength += uint32(hex.EncodedLen(9000)) } // Create the signature object var signature_object []byte switch context.SignData.Signature.CertType { case TimeStampSignature: signature_object = context.createTimestampPlaceholder() default: signature_object = context.createSignaturePlaceholder() } // Write the new signature object context.SignData.objectId, err = context.addObject(signature_object) if err != nil { return fmt.Errorf("failed to add signature object: %w", err) } // Create visual signature (visible or invisible based on CertType) visible := false rectangle := [4]float64{0, 0, 0, 0} if context.SignData.Signature.CertType != ApprovalSignature && context.SignData.Appearance.Visible { return fmt.Errorf("visible signatures are only allowed for approval signatures") } else if context.SignData.Signature.CertType == ApprovalSignature && context.SignData.Appearance.Visible { visible = true rectangle = [4]float64{ context.SignData.Appearance.LowerLeftX, context.SignData.Appearance.LowerLeftY, context.SignData.Appearance.UpperRightX, context.SignData.Appearance.UpperRightY, } } // Example usage: passing page number and default rect values visual_signature, err := context.createVisualSignature(visible, context.SignData.Appearance.Page, rectangle) if err != nil { return fmt.Errorf("failed to create visual signature: %w", err) } // Write the new visual signature object. context.VisualSignData.objectId, err = context.addObject(visual_signature) if err != nil { return fmt.Errorf("failed to add visual signature object: %w", err) } if context.SignData.Appearance.Visible { inc_page_update, err := context.createIncPageUpdate(context.SignData.Appearance.Page, context.VisualSignData.objectId) if err != nil { return fmt.Errorf("failed to create incremental page update: %w", err) } err = context.updateObject(context.VisualSignData.pageObjectId, inc_page_update) if err != nil { return fmt.Errorf("failed to add incremental page update object: %w", err) } } // Create a new catalog object catalog, err := context.createCatalog() if err != nil { return fmt.Errorf("failed to create catalog: %w", err) } // Write the new catalog object context.CatalogData.ObjectId, err = context.addObject(catalog) if err != nil { return fmt.Errorf("failed to add catalog object: %w", err) } // Write xref table if err := context.writeXref(); err != nil { return fmt.Errorf("failed to write xref: %w", err) } // Write trailer if err := context.writeTrailer(); err != nil { return fmt.Errorf("failed to write trailer: %w", err) } // Update byte range if err := context.updateByteRange(); err != nil { return fmt.Errorf("failed to update byte range: %w", err) } // Replace signature if err := context.replaceSignature(); err != nil { return fmt.Errorf("failed to replace signature: %w", err) } // Write final output if _, err := context.OutputBuffer.Seek(0, 0); err != nil { return err } file_content := context.OutputBuffer.Buff.Bytes() if _, err := context.OutputFile.Write(file_content); err != nil { return err } return nil }