From 41a9627fee2556e17e1890b1191a0f08d0296881 Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Tue, 4 Jul 2017 20:44:52 +0200 Subject: [PATCH 01/16] Add sign functionality --- sign.go | 117 ++++++ sign/helpers.go | 109 ++++++ sign/pdfbyterange.go | 45 +++ sign/pdfcatalog.go | 60 +++ sign/pdfsignature.go | 117 ++++++ sign/pdftrailer.go | 44 +++ sign/pdfxref.go | 55 +++ sign/sign.go | 149 ++++++++ signatures.go => verify/verify.go | 608 +++++++++++++++--------------- 9 files changed, 998 insertions(+), 306 deletions(-) create mode 100644 sign.go create mode 100644 sign/helpers.go create mode 100644 sign/pdfbyterange.go create mode 100644 sign/pdfcatalog.go create mode 100644 sign/pdfsignature.go create mode 100644 sign/pdftrailer.go create mode 100644 sign/pdfxref.go create mode 100644 sign/sign.go rename signatures.go => verify/verify.go (91%) diff --git a/sign.go b/sign.go new file mode 100644 index 0000000..9488dac --- /dev/null +++ b/sign.go @@ -0,0 +1,117 @@ +package main + +import ( + "flag" + "log" + "os" + "time" + + "crypto" + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + + "bitbucket.org/digitorus/pdfsign/sign" + "bitbucket.org/digitorus/pdfsign/verify" +) + +func usage() { + log.Fatal("Usage: sign input.pdf output.pdf certificate.crt private_key.key OR verify input.pdf") +} + +func main() { + flag.Parse() + + if len(flag.Args()) < 2 { + usage() + } + + method := flag.Arg(0) + if method != "sign" && method != "verify" { + usage() + } + + input := flag.Arg(1) + if len(input) == 0 { + usage() + } + + if method == "verify" { + input_file, err := os.Open(input) + if err != nil { + log.Fatal(err) + } + defer input_file.Close() + + resp, err := verify.Verify(input_file) + log.Println(resp) + if err != nil { + log.Println(err) + } + } + + if method == "sign" { + if len(flag.Args()) < 5 { + usage() + } + + output := flag.Arg(2) + if len(output) == 0 { + usage() + } + + certificate_data, err := ioutil.ReadFile(flag.Arg(3)) + if err != nil { + log.Fatal(err) + + } + certificate_data_block, _ := pem.Decode(certificate_data) + if certificate_data_block == nil { + log.Fatal(errors.New("failed to parse PEM block containing the certificate")) + } + + cert, err := x509.ParseCertificate(certificate_data_block.Bytes) + if err != nil { + log.Fatal(err) + } + + key_data, err := ioutil.ReadFile(flag.Arg(4)) + if err != nil { + log.Fatal(err) + } + key_data_block, _ := pem.Decode(key_data) + if key_data_block == nil { + log.Fatal(errors.New("failed to parse PEM block containing the private key")) + } + + pkey, err := x509.ParsePKCS8PrivateKey(key_data_block.Bytes) + if err != nil { + log.Fatal(err) + } + + key, ok := pkey.(crypto.Signer) + if !ok { + log.Fatal(errors.New("private key does not implement crypto.Signer")) + } + + err = sign.SignFile(input, output, sign.SignData{ + Signature: sign.SignDataSignature{ + Info: sign.SignDataSignatureInfo{ + Name: "Jeroen Bobbeldijk", + Location: "Rotterdam", + Reason: "Test", + ContactInfo: "Geen", + Date: time.Now().Local(), + }, + }, + Signer: key, + Certificate: cert, + }) + if err != nil { + log.Println(err) + } else { + log.Println("Signed PDF written to " + output) + } + } +} diff --git a/sign/helpers.go b/sign/helpers.go new file mode 100644 index 0000000..fb1c22d --- /dev/null +++ b/sign/helpers.go @@ -0,0 +1,109 @@ +package sign + +import ( + "fmt" + "io" + "math" + "os" + "strings" + "time" +) + +func pdfString(text string) string { + text = strings.Replace(text, "\\", "\\\\", -1) + text = strings.Replace(text, ")", "\\)", -1) + text = strings.Replace(text, "(", "\\(", -1) + text = strings.Replace(text, "\r", "\\r", -1) + + text = "(" + text + ")" + + return text +} + +func pdfDateTime(date time.Time) string { + // Calculate timezone offset from GMT. + _, original_offset := date.Zone() + offset := original_offset + if offset < 0 { + offset = (offset - offset) - offset + } + + offset_duration := time.Duration(offset) * time.Second + offset_hours := int(math.Floor(offset_duration.Hours())) + offset_minutes := int(math.Floor(offset_duration.Minutes())) + offset_minutes = offset_minutes - (offset_hours * 60) + + dateString := "D:" + date.Format("20060102150405") + + // Do some special formatting as the PDF timezone format isn't supported by Go. + if original_offset < 0 { + dateString += "-" + } else { + dateString += "+" + } + + offset_hours_formatted := fmt.Sprintf("%d", offset_hours) + offset_minutes_formatted := fmt.Sprintf("%d", offset_minutes) + dateString += leftPad(offset_hours_formatted, "0", 2-len(offset_hours_formatted)) + "'" + leftPad(offset_minutes_formatted, "0", 2-len(offset_minutes_formatted)) + "'" + + return pdfString(dateString) +} + +func leftPad(s string, padStr string, pLen int) string { + return strings.Repeat(padStr, pLen) + s +} + +func writePartFromSourceFileToTargetFile(input_file *os.File, output_file *os.File, offset int64, length int64) error { + input_file.Seek(offset, 0) + + // Create a small buffer for proper IO handling. + max_chunk_length := int64(1024) + + // If the target length is smaller than our chunk size, use that as chunk size. + if length < max_chunk_length { + max_chunk_length = length + } + + // Track read/written bytes so we know when we're done. + read_bytes := int64(0) + + // Create a buffer for the chunks. + buf := make([]byte, max_chunk_length) + for { + // Read the chunk from the input file. + n, err := input_file.Read(buf) + if err != nil && err != io.EOF { + return err + } + + // If we got to the end of the file, break. + if err == io.EOF { + break + } + + // If nothing was read, break. + if n == 0 { + break + } + + // Write the chunk to the output file. + if _, err := output_file.Write(buf[:n]); err != nil { + return err + } + + read_bytes += int64(n) + + // If we read enough bytes, break. + if read_bytes >= length { + break + } + + // If our next chunk will be too big, make a smaller buffer. + // If we won't do this, we might end up with more data than we want. + if length-read_bytes < max_chunk_length { + buf = make([]byte, length-read_bytes) + } + } + + return nil +} diff --git a/sign/pdfbyterange.go b/sign/pdfbyterange.go new file mode 100644 index 0000000..3e0b58d --- /dev/null +++ b/sign/pdfbyterange.go @@ -0,0 +1,45 @@ +package sign + +import ( + "fmt" + "strings" +) + +func (context *SignContext) updateByteRange() error { + // Get current filesize. Easier than what should be the current size. + // @todo: find out of this is safe. + output_file_stat, _ := context.OutputFile.Stat() + + // Don't count last newline as file length. + output_file_size := output_file_stat.Size() - 1 + + // Calculate ByteRange values to replace them. + context.ByteRangeValues = make([]int64, 4) + + // Signature ByteRange part 1 start byte is always byte 0. + context.ByteRangeValues[0] = int64(0) + + // Signature ByteRange part 1 length always stops at the actual signature start byte. + context.ByteRangeValues[1] = context.SignatureContentsStartByte + + // Signature ByteRange part 2 start byte directly starts after the actual signature. + context.ByteRangeValues[2] = context.ByteRangeValues[1] + int64(signatureMaxLength) + + // Signature ByteRange part 2 length is everything else of the file. + context.ByteRangeValues[3] = output_file_size - context.ByteRangeValues[2] + + new_byte_range := fmt.Sprintf("/ByteRange[%d %d %d %d]", context.ByteRangeValues[0], context.ByteRangeValues[1], context.ByteRangeValues[2], context.ByteRangeValues[3]) + + // Make sure our ByteRange string didn't shrink in length. + new_byte_range += strings.Repeat(" ", len(signatureByteRangePlaceholder)-len(new_byte_range)) + + // Seek to ByteRange position in file. + context.OutputFile.Seek(context.ByteRangeStartByte, 0) + + // Write new ByteRange. + if _, err := context.OutputFile.Write([]byte(new_byte_range)); err != nil { + return err + } + + return nil +} diff --git a/sign/pdfcatalog.go b/sign/pdfcatalog.go new file mode 100644 index 0000000..f027527 --- /dev/null +++ b/sign/pdfcatalog.go @@ -0,0 +1,60 @@ +package sign + +import ( + "errors" + "strconv" +) + +func (context *SignContext) createCatalog() (catalog string, err error) { + catalog = strconv.Itoa(int(context.CatalogData.ObjectId)) + " 0 obj\n" + catalog += "<< /Type /Catalog" + catalog += " /Version /" + context.PDFReader.PDFVersion + + root := context.PDFReader.Trailer().Key("Root") + root_keys := root.Keys() + found_pages := false + for _, key := range root_keys { + if key == "Pages" { + found_pages = true + break + } + } + + if !found_pages { + return "", errors.New("Didn't find pages in PDF trailer Root.") + } + + rootPtr := root.GetPtr() + context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R" + + pages := root.Key("Pages").GetPtr() + catalog += " /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R" + catalog += " /AcroForm <<" + catalog += " /Fields [" + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R]" + + if !context.SignData.Signature.Approval { + catalog += " /NeedAppearances false" + } + + if context.SignData.Signature.CertType > 0 { + catalog += " /SigFlags 3" + } else { + catalog += " /SigFlags 1" + } + + catalog += " >>" + + // @todo: what do these do? + if !context.SignData.Signature.Approval { + if context.SignData.Signature.CertType > 0 { + //catalog += " /Perms << /DocMDP " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; + } else { + //catalog += " /Perms << /UR3 " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; + } + } + + catalog += " >>" + catalog += "\nendobj\n" + + return catalog, nil +} diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go new file mode 100644 index 0000000..c6b2401 --- /dev/null +++ b/sign/pdfsignature.go @@ -0,0 +1,117 @@ +package sign + +import ( + "bytes" + "encoding/hex" + "io" + "strconv" + "strings" + + "github.com/digitorus/pkcs7" +) + +var signatureMaxLength = uint32(11742) +var signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]" + +func (context *SignContext) createSignaturePlaceholder() (signature string, byte_range_start_byte int64, signature_contents_start_byte int64) { + signature = strconv.Itoa(int(context.SignData.ObjectId)) + " 0 obj\n" + signature += "<< /Type /Sig" + signature += " /Filter /Adobe.PPKLite" + signature += " /SubFilter /adbe.pkcs7.detached" + + byte_range_start_byte = int64(len(signature)) + 1 + + // Create a placeholder for the byte range string, we will replace it later. + signature += " " + signatureByteRangePlaceholder + + signature_contents_start_byte = int64(len(signature)) + 11 + + // Create a placeholder for the actual signature content, we wil replace it later. + signature += " /Contents<" + strings.Repeat("0", int(signatureMaxLength)) + ">" + + if context.SignData.Signature.Approval { + signature += " /Reference [" // array of signature reference dictionaries + signature += " << /Type /SigRef" + if context.SignData.Signature.CertType > 0 { + signature += " /TransformMethod /DocMDP" + signature += " /TransformParams <<" + signature += " /Type /TransformParams" + signature += " /P " + strconv.Itoa(int(context.SignData.Signature.CertType)) + signature += " /V /1.2" + } else { + signature += " /TransformMethod /UR3" + signature += " /TransformParams <<" + signature += " /Type /TransformParams" + signature += " /V /2.2" + } + + signature += " >>" // close TransformParams + signature += " >>" + signature += " ]" // end of reference + } + + if context.SignData.Signature.Info.Name != "" { + signature += " /Name " + pdfString(context.SignData.Signature.Info.Name) + } + if context.SignData.Signature.Info.Location != "" { + signature += " /Location " + pdfString(context.SignData.Signature.Info.Location) + } + if context.SignData.Signature.Info.Reason != "" { + signature += " /Reason " + pdfString(context.SignData.Signature.Info.Reason) + } + if context.SignData.Signature.Info.ContactInfo != "" { + signature += " /ContactInfo " + pdfString(context.SignData.Signature.Info.ContactInfo) + } + signature += " /M " + pdfDateTime(context.SignData.Signature.Info.Date) + signature += " >>" + signature += "\nendobj\n" + + return signature, byte_range_start_byte, signature_contents_start_byte +} + +func (context *SignContext) createSignature() ([]byte, error) { + + // Sadly we can't efficiently sign a file, we need to read all the bytes we want to sign. + context.OutputFile.Seek(0, 0) + sign_buf := bytes.NewBuffer(nil) + io.Copy(sign_buf, context.OutputFile) + file_content := sign_buf.Bytes() + + // Remove trailing newline. + file_content = file_content[:len(file_content)-1] + + // Collect the parts to sign. + sign_content := make([]byte, context.ByteRangeValues[1]+context.ByteRangeValues[3]) + sign_content = append(sign_content, file_content[context.ByteRangeValues[0]:(context.ByteRangeValues[0]+context.ByteRangeValues[1])]...) + sign_content = append(sign_content, file_content[context.ByteRangeValues[2]:(context.ByteRangeValues[2]+context.ByteRangeValues[3])]...) + + // Initialize pkcs7 signer. + signed_data, err := pkcs7.NewSignedData(sign_content) + if err != nil { + return nil, err + } + + // Add the signer and sign the data. + if err := signed_data.AddSignerChain(context.SignData.Certificate, context.SignData.Signer, context.SignData.CertificateChain, pkcs7.SignerInfoConfig{}); err != nil { + return nil, err + } + + // PDF needs a detached signature, meaning the content isn't included. + signed_data.Detach() + + return signed_data.Finish() +} + +func (context *SignContext) replaceSignature() error { + signature, err := context.createSignature() + if err != nil { + return err + } + + dst := make([]byte, hex.EncodedLen(len(signature))) + hex.Encode(dst, signature) + + context.OutputFile.WriteAt(dst, context.ByteRangeValues[0]+context.ByteRangeValues[1]) + + return nil +} diff --git a/sign/pdftrailer.go b/sign/pdftrailer.go new file mode 100644 index 0000000..7332feb --- /dev/null +++ b/sign/pdftrailer.go @@ -0,0 +1,44 @@ +package sign + +import ( + "strconv" + "strings" +) + +func (context *SignContext) writeTrailer() error { + trailer_length := context.PDFReader.XrefInformation.IncludingTrailerEndPos - context.PDFReader.XrefInformation.EndPos + + // Read the trailer so we can replace the size. + context.InputFile.Seek(context.PDFReader.XrefInformation.EndPos+1, 0) + trailer_buf := make([]byte, trailer_length) + if _, err := context.InputFile.Read(trailer_buf); err != nil { + return err + } + + root_string := "Root " + context.CatalogData.RootString + new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R" + + size_string := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) + new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+2, 10) + + trailer_string := string(trailer_buf) + trailer_string = strings.Replace(trailer_string, root_string, new_root, -1) + trailer_string = strings.Replace(trailer_string, size_string, new_size, -1) + + // Write the new trailer. + if _, err := context.OutputFile.Write([]byte(trailer_string)); err != nil { + return err + } + + // Write the new xref start position. + if _, err := context.OutputFile.Write([]byte(strconv.FormatInt(context.NewXrefStart, 10) + "\n")); err != nil { + return err + } + + // Write PDF ending. + if _, err := context.OutputFile.Write([]byte("%%EOF\n")); err != nil { + return err + } + + return nil +} diff --git a/sign/pdfxref.go b/sign/pdfxref.go new file mode 100644 index 0000000..cf2c66f --- /dev/null +++ b/sign/pdfxref.go @@ -0,0 +1,55 @@ +package sign + +import ( + "errors" + "strconv" +) + +func (context *SignContext) writeXref() error { + + // @todo: support stream xref. + + if context.PDFReader.XrefInformation.Type == "table" { + if err := context.writeXrefTable(); err != nil { + return err + } + } else { + return errors.New("Unkwn xref type: " + context.PDFReader.XrefInformation.Type) + } + + return nil +} + +func (context *SignContext) writeXrefTable() error { + xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) + new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+2, 10) + + if _, err := context.OutputFile.Write([]byte(new_xref_size)); err != nil { + return err + } + + // Write the old xref table to the output pdf. + if err := writePartFromSourceFileToTargetFile(context.InputFile, context.OutputFile, context.PDFReader.XrefInformation.StartPos+int64(len(xref_size)), context.PDFReader.XrefInformation.Length-int64(len(xref_size))); err != nil { + return err + } + + // Create the new catalog xref line. + catalog_object_start_position := strconv.FormatInt(context.PDFReader.XrefInformation.StartPos, 10) + catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n\n" + + // Write the new catalog xref line. + if _, err := context.OutputFile.Write([]byte(catalog_xref_line)); err != nil { + return err + } + + // Create the new signature xref line. + signature_object_start_position := strconv.FormatInt(context.PDFReader.XrefInformation.StartPos+context.CatalogData.Length, 10) + signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n\n" + + // Write the new signature xref line. + if _, err := context.OutputFile.Write([]byte(signature_xref_line)); err != nil { + return err + } + + return nil +} diff --git a/sign/sign.go b/sign/sign.go new file mode 100644 index 0000000..51b4c8a --- /dev/null +++ b/sign/sign.go @@ -0,0 +1,149 @@ +package sign + +import ( + "crypto" + "crypto/x509" + "os" + "time" + + "bitbucket.org/digitorus/pdf" +) + +type CatalogData struct { + ObjectId uint32 + Length int64 + RootString string +} + +type SignData struct { + ObjectId uint32 + Signature SignDataSignature + Signer crypto.Signer + Certificate *x509.Certificate + CertificateChain []*x509.Certificate +} + +type SignDataSignature struct { + Approval bool + CertType uint32 + Info SignDataSignatureInfo +} + +type SignDataSignatureInfo struct { + Name string + Location string + Reason string + ContactInfo string + Date time.Time +} + +type SignContext struct { + InputFile *os.File + OutputFile *os.File + SignData SignData + CatalogData CatalogData + PDFReader *pdf.Reader + NewXrefStart int64 + ByteRangeStartByte int64 + SignatureContentsStartByte int64 + ByteRangeValues []int64 +} + +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 + } + + sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 1 + + context := SignContext{ + PDFReader: rdr, + InputFile: input_file, + OutputFile: output_file, + CatalogData: CatalogData{ + ObjectId: uint32(rdr.XrefInformation.ItemCount), + }, + SignData: sign_data, + } + + err = context.SignPDF() + if err != nil { + return err + } + + return nil +} + +func (context *SignContext) SignPDF() error { + // Write the PDF file to the output up til the xref. + if err := writePartFromSourceFileToTargetFile(context.InputFile, context.OutputFile, 0, context.PDFReader.XrefInformation.StartPos); err != nil { + return err + } + + catalog, err := context.createCatalog() + if err != nil { + return err + } + + context.CatalogData.Length = int64(len(catalog)) + + // Write the new catalog object. + if _, err := context.OutputFile.Write([]byte(catalog)); err != nil { + return err + } + + // Create the signature object + signature_object, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder() + + // Positions are relative to old start position of xref table. + byte_range_start_byte += context.PDFReader.XrefInformation.StartPos + int64(len(catalog)) + signature_contents_start_byte += context.PDFReader.XrefInformation.StartPos + int64(len(catalog)) + + context.ByteRangeStartByte = byte_range_start_byte + context.SignatureContentsStartByte = signature_contents_start_byte + + // Write the new signature object. + if _, err := context.OutputFile.Write([]byte(signature_object)); err != nil { + return err + } + + // Calculate the new start position of the xref table. + context.NewXrefStart = context.PDFReader.XrefInformation.StartPos + int64(len(signature_object)) + int64(len(catalog)) + + if err := context.writeXref(); err != nil { + return err + } + + if err := context.writeTrailer(); err != nil { + return err + } + + if err := context.updateByteRange(); err != nil { + return err + } + + if err := context.replaceSignature(); err != nil { + return err + } + + return nil +} diff --git a/signatures.go b/verify/verify.go similarity index 91% rename from signatures.go rename to verify/verify.go index e3cad70..4635a0e 100644 --- a/signatures.go +++ b/verify/verify.go @@ -1,306 +1,302 @@ -// http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf - -package signatures - -import ( - "bytes" - "crypto/x509" - "encoding/asn1" - "fmt" - "io" - "os" - "io/ioutil" -// "log" -// "strings" - "time" - - "bitbucket.org/digitorus/pdf" - "github.com/digitorus/timestamp" - "go.mozilla.org/pkcs7" - "golang.org/x/crypto/ocsp" -) - -type RevocationInfoArchival struct { - CRL RevCRL `asn1:"tag:0,optional,explicit"` - OCSP RevOCSP `asn1:"tag:1,optional,explicit"` - OtherRevInfo OtherRevInfo `asn1:"tag:2,optional,explicit"` -} - -type RevCRL []asn1.RawValue -type RevOCSP []asn1.RawValue - -type OtherRevInfo struct { - Type asn1.ObjectIdentifier - Value []byte -} - -type Response struct { - Error string - - DocumentInfo string - Signers []Signer -} - -type Signer struct { - Name string - Reason string - Location string - ContactInfo string - ValidSignature bool - TrustedIssuer bool - RevokedCertificate bool - Certificates []Certificate - TimeStamp *timestamp.Timestamp -} - -type Certificate struct { - Certificate *x509.Certificate - VerifyError string - OCSPResponse *ocsp.Response - OCSPEmbedded bool - CRLRevoked time.Time - CRLEmbedded bool -} - -func Verify(file *os.File) (apiResp *Response, err error) { - defer func() { - if r := recover(); r != nil { - apiResp = nil - err = fmt.Errorf("Failed to verify file (%v)", r) - } - }() - apiResp = &Response{} - - finfo, _ := file.Stat() - size := finfo.Size() - - rdr, err := pdf.NewReader(file, size) - if err != nil { - return nil, fmt.Errorf("Failed to open file") - } - - // AcroForm will contain a SigFlags value if the form contains a digital signature - t := rdr.Trailer().Key("Root").Key("AcroForm").Key("SigFlags") - if t.IsNull() { - return nil, fmt.Errorf("No digital signature in document") - } - - // Walk over the cross references in the document - for _, x := range rdr.Xref() { - // Get the xref object Value - v := rdr.Resolve(x.Ptr(), x.Ptr()) - - // We must have a Filter Adobe.PPKLite - if v.Key("Filter").Name() != "Adobe.PPKLite" { - continue - } - - signer := Signer{ - Name: v.Key("Name").Text(), - Reason: v.Key("Reason").Text(), - Location: v.Key("Location").Text(), - ContactInfo: v.Key("ContactInfo").Text(), - } - - // (Required) The signature value. When ByteRange is present, the - // value shall be a hexadecimal string (see 7.3.4.3, “Hexadecimal - // Strings”) representing the value of the byte range digest. - // For public-key signatures, Contents should be either a DER-encoded - // PKCS#1 binary data object or a DER-encoded PKCS#7 binary data object. - // Space for the Contents value must be allocated before the message - // digest is computed. (See 7.3.4, “String Objects“) - p7, err := pkcs7.Parse([]byte(v.Key("Contents").RawString())) - if err != nil { - //fmt.Println(err) - continue - } - - // An array of pairs of integers (starting byte offset, length in - // bytes) that shall describe the exact byte range for the digest - // calculation. Multiple discontiguous byte ranges shall be used to - // describe a digest that does not include the signature value (the - // Contents entry) itself. - for i := 0; i < v.Key("ByteRange").Len(); i++ { - // As the byte range comes in pairs, we increment one extra - i++ - - // Read the byte range from the raw file and add it to the contents. - // This content will be hashed with the corresponding algorithm to - // verify the signature. - content, err := ioutil.ReadAll(io.NewSectionReader(file, v.Key("ByteRange").Index(i-1).Int64(), v.Key("ByteRange").Index(i).Int64())) - if err != nil { - apiResp.Error = fmt.Sprintln("Failed to get ByteRange:", i, err) - } - p7.Content = append(p7.Content, content...) - } - - // Signer certificate - // http://www.alvestrand.no/objectid/1.2.840.113549.1.9.html - // http://www.alvestrand.no/objectid/1.2.840.113583.1.1.8.html - var isn []byte - for _, s := range p7.Signers { - isn = s.IssuerAndSerialNumber.IssuerName.FullBytes - //for _, a := range s.AuthenticatedAttributes { - //fmt.Printf("A: %v, %#v\n", s.IssuerAndSerialNumber.SerialNumber, a.Type) - //} - - // Timestamp - // http://www.alvestrand.no/objectid/1.2.840.113549.1.9.16.2.14.html - // Timestamp - // 1.2.840.113549.1.9.16.2.14 - RFC 3161 id-aa-timeStampToken - for _, attr := range s.UnauthenticatedAttributes { - //fmt.Printf("U: %v, %#v\n", s.IssuerAndSerialNumber.SerialNumber, attr.Type) - - if attr.Type.Equal(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}) { - //fmt.Println("Found timestamp") - - signer.TimeStamp, err = timestamp.Parse(attr.Value.Bytes) - if err != nil { - apiResp.Error = fmt.Sprintln("Failed to parse timestamp", err) - } - - break - } - } - } - - // Directory of certificates, including OCSP - //var ica *x509.Certificate - certPool := x509.NewCertPool() - for _, cert := range p7.Certificates { - certPool.AddCert(cert) - if bytes.Equal(isn, cert.RawSubject) { - //ica = cert - } - } - - // Verify the digital signature of the pdf file. - err = p7.VerifyWithChain(certPool) - if err != nil { - err = p7.Verify() - if err == nil { - signer.ValidSignature = true - signer.TrustedIssuer = false - } - //apiResp.Error = fmt.Sprintln("Failed to verify signature:", err) - } else { - signer.ValidSignature = true - signer.TrustedIssuer = true - } - - // PDF signature certificate revocation information attribute (1.2.840.113583.1.1.8) - var revInfo RevocationInfoArchival - p7.UnmarshalSignedAttribute(asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, &revInfo) - - // Parse OCSP response - var ocspStatus = make(map[string]*ocsp.Response) - for _, o := range revInfo.OCSP { - resp, err := ocsp.ParseResponse(o.FullBytes, nil) - if err != nil { - apiResp.Error = fmt.Sprintln("Failed to parse or verify OCSP response", err) - ocspStatus[fmt.Sprintf("%x", resp.SerialNumber)] = nil - } else { - ocspStatus[fmt.Sprintf("%x", resp.SerialNumber)] = resp - } - } - - // Build certificate chains and verify revocation status - for _, cert := range p7.Certificates { - var c Certificate - c.Certificate = cert - - chain, err := cert.Verify(x509.VerifyOptions{ - Intermediates: certPool, - CurrentTime: cert.NotBefore, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - }) - - if err != nil { - c.VerifyError = err.Error() - } - - if resp, ok := ocspStatus[fmt.Sprintf("%x", cert.SerialNumber)]; ok { - c.OCSPResponse = resp - c.OCSPEmbedded = true - - if resp.Status != ocsp.Good { - signer.RevokedCertificate = true - } - - if len(chain) > 1 && len(chain[0]) > 1 { - issuer := chain[0][1] - if resp.Certificate != nil { - err = resp.Certificate.CheckSignatureFrom(issuer) - if err != nil { - apiResp.Error = fmt.Sprintln("OCSP signing cerificate not from certificate issuer:", err) - } - } else { - // CA Signed response - err = resp.CheckSignatureFrom(issuer) - if err != nil { - apiResp.Error = fmt.Sprintln("Failed to verify OCSP response signature:", err) - } - } - } - } else { - // Check OCSP status for certificate out of band - } - - // Add certificate to result - signer.Certificates = append(signer.Certificates, c) - } - - // Certificate revocation lists when included in this document - for _, crl := range p7.CRLs { - //var crlissuer *pkix.Name - //crlissuerdr.FillFromRDNSequence(&crl.TBSCertList.Issuer) - if len(crl.TBSCertList.RevokedCertificates) > 0 { - - } - //apiResp.Error = fmt.Sprintf("CRL %v , with %d entries\n", crl.TBSCertList.Issuer, len(crl.TBSCertList.RevokedCertificates)) - // TODO(vanbroup): Check revocation via CRL - // signer.RevokedCertificate = true - } - - // Parse CRL file - for _, c := range revInfo.CRL { - crl, err := x509.ParseCRL(c.FullBytes) - if err != nil { - apiResp.Error = fmt.Sprintln("Failed to parse or verify embedded CRL") - } - - if len(crl.TBSCertList.RevokedCertificates) > 0 { - - } - - //var crlissuer *pkix.Name - //crlissuerdr.FillFromRDNSequence(&crl.TBSCertList.Issuer) - //apiResp.Error = fmt.Sprintf("CRL %v , with %d entries\n", crl.TBSCertList.Issuer, len(crl.TBSCertList.RevokedCertificates)) - // TODO(vanbroup): Check revocation via CRL - // signer.RevokedCertificate = true - } - - // If SubFilter is adbe.pkcs7.detached or adbe.pkcs7.sha1, this entry - // shall not be used, and the certificate chain shall be put in the PKCS#7 - // envelope in Contents. - //v.Key("Cert").Text() - - apiResp.Signers = append(apiResp.Signers, signer) - } - - if apiResp == nil { - err = fmt.Errorf("Document looks to have a signature but got no results") - } - - return -} - -func walk(t pdf.Value, pad int) { - for _, k := range t.Keys() { - v := t.Key(k) - if v.Kind() == pdf.Array || v.Kind() == pdf.Dict { - pad++ - walk(v, pad) - } - } -} +package verify + +import ( + "bytes" + "crypto/x509" + "encoding/asn1" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pkcs7" + "github.com/digitorus/timestamp" + "golang.org/x/crypto/ocsp" +) + +type RevocationInfoArchival struct { + CRL RevCRL `asn1:"tag:0,optional,explicit"` + OCSP RevOCSP `asn1:"tag:1,optional,explicit"` + OtherRevInfo OtherRevInfo `asn1:"tag:2,optional,explicit"` +} + +type RevCRL []asn1.RawValue +type RevOCSP []asn1.RawValue + +type OtherRevInfo struct { + Type asn1.ObjectIdentifier + Value []byte +} + +type Response struct { + Error string + + DocumentInfo string + Signers []Signer +} + +type Signer struct { + Name string + Reason string + Location string + ContactInfo string + ValidSignature bool + TrustedIssuer bool + RevokedCertificate bool + Certificates []Certificate + TimeStamp *timestamp.Timestamp +} + +type Certificate struct { + Certificate *x509.Certificate + VerifyError string + OCSPResponse *ocsp.Response + OCSPEmbedded bool + CRLRevoked time.Time + CRLEmbedded bool +} + +func Verify(file *os.File) (apiResp *Response, err error) { + defer func() { + if r := recover(); r != nil { + apiResp = nil + err = fmt.Errorf("Failed to verify file (%v)", r) + } + }() + apiResp = &Response{} + + finfo, _ := file.Stat() + size := finfo.Size() + + rdr, err := pdf.NewReader(file, size) + if err != nil { + return nil, fmt.Errorf("Failed to open file") + } + + // AcroForm will contain a SigFlags value if the form contains a digital signature + t := rdr.Trailer().Key("Root").Key("AcroForm").Key("SigFlags") + if t.IsNull() { + return nil, fmt.Errorf("No digital signature in document") + } + + // Walk over the cross references in the document + for _, x := range rdr.Xref() { + // Get the xref object Value + v := rdr.Resolve(x.Ptr(), x.Ptr()) + + // We must have a Filter Adobe.PPKLite + if v.Key("Filter").Name() != "Adobe.PPKLite" { + continue + } + + signer := Signer{ + Name: v.Key("Name").Text(), + Reason: v.Key("Reason").Text(), + Location: v.Key("Location").Text(), + ContactInfo: v.Key("ContactInfo").Text(), + } + + // (Required) The signature value. When ByteRange is present, the + // value shall be a hexadecimal string (see 7.3.4.3, “Hexadecimal + // Strings”) representing the value of the byte range digest. + // For public-key signatures, Contents should be either a DER-encoded + // PKCS#1 binary data object or a DER-encoded PKCS#7 binary data object. + // Space for the Contents value must be allocated before the message + // digest is computed. (See 7.3.4, “String Objects“) + p7, err := pkcs7.Parse([]byte(v.Key("Contents").RawString())) + if err != nil { + //fmt.Println(err) + continue + } + + // An array of pairs of integers (starting byte offset, length in + // bytes) that shall describe the exact byte range for the digest + // calculation. Multiple discontiguous byte ranges shall be used to + // describe a digest that does not include the signature value (the + // Contents entry) itself. + for i := 0; i < v.Key("ByteRange").Len(); i++ { + // As the byte range comes in pairs, we increment one extra + i++ + + // Read the byte range from the raw file and add it to the contents. + // This content will be hashed with the corresponding algorithm to + // verify the signature. + content, err := ioutil.ReadAll(io.NewSectionReader(file, v.Key("ByteRange").Index(i-1).Int64(), v.Key("ByteRange").Index(i).Int64())) + if err != nil { + apiResp.Error = fmt.Sprintln("Failed to get ByteRange:", i, err) + } + p7.Content = append(p7.Content, content...) + } + + // Signer certificate + // http://www.alvestrand.no/objectid/1.2.840.113549.1.9.html + // http://www.alvestrand.no/objectid/1.2.840.113583.1.1.8.html + var isn []byte + for _, s := range p7.Signers { + isn = s.IssuerAndSerialNumber.IssuerName.FullBytes + //for _, a := range s.AuthenticatedAttributes { + //fmt.Printf("A: %v, %#v\n", s.IssuerAndSerialNumber.SerialNumber, a.Type) + //} + + // Timestamp + // http://www.alvestrand.no/objectid/1.2.840.113549.1.9.16.2.14.html + // Timestamp + // 1.2.840.113549.1.9.16.2.14 - RFC 3161 id-aa-timeStampToken + for _, attr := range s.UnauthenticatedAttributes { + //fmt.Printf("U: %v, %#v\n", s.IssuerAndSerialNumber.SerialNumber, attr.Type) + + if attr.Type.Equal(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}) { + //fmt.Println("Found timestamp") + + signer.TimeStamp, err = timestamp.Parse(attr.Value.Bytes) + if err != nil { + apiResp.Error = fmt.Sprintln("Failed to parse timestamp", err) + } + + break + } + } + } + + // Directory of certificates, including OCSP + //var ica *x509.Certificate + certPool := x509.NewCertPool() + for _, cert := range p7.Certificates { + certPool.AddCert(cert) + if bytes.Equal(isn, cert.RawSubject) { + //ica = cert + } + } + + // Verify the digital signature of the pdf file. + err = p7.VerifyWithChain(certPool) + if err != nil { + err = p7.Verify() + if err == nil { + signer.ValidSignature = true + signer.TrustedIssuer = false + } + //apiResp.Error = fmt.Sprintln("Failed to verify signature:", err) + } else { + signer.ValidSignature = true + signer.TrustedIssuer = true + } + + // PDF signature certificate revocation information attribute (1.2.840.113583.1.1.8) + var revInfo RevocationInfoArchival + p7.UnmarshalSignedAttribute(asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, &revInfo) + + // Parse OCSP response + var ocspStatus = make(map[string]*ocsp.Response) + for _, o := range revInfo.OCSP { + resp, err := ocsp.ParseResponse(o.FullBytes, nil) + if err != nil { + apiResp.Error = fmt.Sprintln("Failed to parse or verify OCSP response", err) + ocspStatus[fmt.Sprintf("%x", resp.SerialNumber)] = nil + } else { + ocspStatus[fmt.Sprintf("%x", resp.SerialNumber)] = resp + } + } + + // Build certificate chains and verify revocation status + for _, cert := range p7.Certificates { + var c Certificate + c.Certificate = cert + + chain, err := cert.Verify(x509.VerifyOptions{ + Intermediates: certPool, + CurrentTime: cert.NotBefore, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + + if err != nil { + c.VerifyError = err.Error() + } + + if resp, ok := ocspStatus[fmt.Sprintf("%x", cert.SerialNumber)]; ok { + c.OCSPResponse = resp + c.OCSPEmbedded = true + + if resp.Status != ocsp.Good { + signer.RevokedCertificate = true + } + + if len(chain) > 1 && len(chain[0]) > 1 { + issuer := chain[0][1] + if resp.Certificate != nil { + err = resp.Certificate.CheckSignatureFrom(issuer) + if err != nil { + apiResp.Error = fmt.Sprintln("OCSP signing cerificate not from certificate issuer:", err) + } + } else { + // CA Signed response + err = resp.CheckSignatureFrom(issuer) + if err != nil { + apiResp.Error = fmt.Sprintln("Failed to verify OCSP response signature:", err) + } + } + } + } else { + // Check OCSP status for certificate out of band + } + + // Add certificate to result + signer.Certificates = append(signer.Certificates, c) + } + + // Certificate revocation lists when included in this document + for _, crl := range p7.CRLs { + //var crlissuer *pkix.Name + //crlissuerdr.FillFromRDNSequence(&crl.TBSCertList.Issuer) + if len(crl.TBSCertList.RevokedCertificates) > 0 { + + } + //apiResp.Error = fmt.Sprintf("CRL %v , with %d entries\n", crl.TBSCertList.Issuer, len(crl.TBSCertList.RevokedCertificates)) + // TODO(vanbroup): Check revocation via CRL + // signer.RevokedCertificate = true + } + + // Parse CRL file + for _, c := range revInfo.CRL { + crl, err := x509.ParseCRL(c.FullBytes) + if err != nil { + apiResp.Error = fmt.Sprintln("Failed to parse or verify embedded CRL") + } + + if len(crl.TBSCertList.RevokedCertificates) > 0 { + + } + + //var crlissuer *pkix.Name + //crlissuerdr.FillFromRDNSequence(&crl.TBSCertList.Issuer) + //apiResp.Error = fmt.Sprintf("CRL %v , with %d entries\n", crl.TBSCertList.Issuer, len(crl.TBSCertList.RevokedCertificates)) + // TODO(vanbroup): Check revocation via CRL + // signer.RevokedCertificate = true + } + + // If SubFilter is adbe.pkcs7.detached or adbe.pkcs7.sha1, this entry + // shall not be used, and the certificate chain shall be put in the PKCS#7 + // envelope in Contents. + //v.Key("Cert").Text() + + apiResp.Signers = append(apiResp.Signers, signer) + } + + if apiResp == nil { + err = fmt.Errorf("Document looks to have a signature but got no results") + } + + return +} + +func walk(t pdf.Value, pad int) { + for _, k := range t.Keys() { + v := t.Key(k) + if v.Kind() == pdf.Array || v.Kind() == pdf.Dict { + pad++ + walk(v, pad) + } + } +} From 3eedc089d0dd0e1cb58d6f7762a8f2fd89279cf3 Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Tue, 4 Jul 2017 21:59:13 +0200 Subject: [PATCH 02/16] Fix signature padding, return verify error in validator --- sign/pdfsignature.go | 2 +- verify/verify.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index c6b2401..0f19ff4 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -81,7 +81,7 @@ func (context *SignContext) createSignature() ([]byte, error) { file_content = file_content[:len(file_content)-1] // Collect the parts to sign. - sign_content := make([]byte, context.ByteRangeValues[1]+context.ByteRangeValues[3]) + sign_content := make([]byte, 0) sign_content = append(sign_content, file_content[context.ByteRangeValues[0]:(context.ByteRangeValues[0]+context.ByteRangeValues[1])]...) sign_content = append(sign_content, file_content[context.ByteRangeValues[2]:(context.ByteRangeValues[2]+context.ByteRangeValues[3])]...) diff --git a/verify/verify.go b/verify/verify.go index 4635a0e..5533e17 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -123,10 +123,12 @@ func Verify(file *os.File) (apiResp *Response, err error) { // Read the byte range from the raw file and add it to the contents. // This content will be hashed with the corresponding algorithm to // verify the signature. + content, err := ioutil.ReadAll(io.NewSectionReader(file, v.Key("ByteRange").Index(i-1).Int64(), v.Key("ByteRange").Index(i).Int64())) if err != nil { apiResp.Error = fmt.Sprintln("Failed to get ByteRange:", i, err) } + p7.Content = append(p7.Content, content...) } @@ -178,7 +180,7 @@ func Verify(file *os.File) (apiResp *Response, err error) { signer.ValidSignature = true signer.TrustedIssuer = false } - //apiResp.Error = fmt.Sprintln("Failed to verify signature:", err) + apiResp.Error = fmt.Sprintln("Failed to verify signature:", err) } else { signer.ValidSignature = true signer.TrustedIssuer = true From 6d38be3f51adf739c99b7a4d488de150eff839dc Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Tue, 4 Jul 2017 22:10:09 +0200 Subject: [PATCH 03/16] Use pkcs1 private key parser --- sign.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sign.go b/sign.go index 9488dac..3aec357 100644 --- a/sign.go +++ b/sign.go @@ -6,7 +6,6 @@ import ( "os" "time" - "crypto" "crypto/x509" "encoding/pem" "errors" @@ -85,16 +84,11 @@ func main() { log.Fatal(errors.New("failed to parse PEM block containing the private key")) } - pkey, err := x509.ParsePKCS8PrivateKey(key_data_block.Bytes) + pkey, err := x509.ParsePKCS1PrivateKey(key_data_block.Bytes) if err != nil { log.Fatal(err) } - key, ok := pkey.(crypto.Signer) - if !ok { - log.Fatal(errors.New("private key does not implement crypto.Signer")) - } - err = sign.SignFile(input, output, sign.SignData{ Signature: sign.SignDataSignature{ Info: sign.SignDataSignatureInfo{ @@ -105,7 +99,7 @@ func main() { Date: time.Now().Local(), }, }, - Signer: key, + Signer: pkey, Certificate: cert, }) if err != nil { From db69b6a4c0dc5e9e57bd8516ffd4d370fb39c8ce Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Sat, 8 Jul 2017 14:55:31 +0200 Subject: [PATCH 04/16] Append instead of replace --- sign.go | 2 ++ sign/pdfbyterange.go | 7 +++-- sign/pdfcatalog.go | 7 +++-- sign/pdfsignature.go | 7 ++--- sign/pdftrailer.go | 4 +-- sign/pdfvisualsignature.go | 44 +++++++++++++++++++++++++++++++ sign/pdfxref.go | 15 ++++++++--- sign/sign.go | 54 +++++++++++++++++++++++++++++++++----- verify/verify.go | 13 ++++++++- 9 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 sign/pdfvisualsignature.go diff --git a/sign.go b/sign.go index 3aec357..698bfb6 100644 --- a/sign.go +++ b/sign.go @@ -98,6 +98,8 @@ func main() { ContactInfo: "Geen", Date: time.Now().Local(), }, + CertType: 2, + Approval: false, }, Signer: pkey, Certificate: cert, diff --git a/sign/pdfbyterange.go b/sign/pdfbyterange.go index 3e0b58d..5bc3550 100644 --- a/sign/pdfbyterange.go +++ b/sign/pdfbyterange.go @@ -10,8 +10,7 @@ func (context *SignContext) updateByteRange() error { // @todo: find out of this is safe. output_file_stat, _ := context.OutputFile.Stat() - // Don't count last newline as file length. - output_file_size := output_file_stat.Size() - 1 + output_file_size := output_file_stat.Size() // Calculate ByteRange values to replace them. context.ByteRangeValues = make([]int64, 4) @@ -20,10 +19,10 @@ func (context *SignContext) updateByteRange() error { context.ByteRangeValues[0] = int64(0) // Signature ByteRange part 1 length always stops at the actual signature start byte. - context.ByteRangeValues[1] = context.SignatureContentsStartByte + context.ByteRangeValues[1] = context.SignatureContentsStartByte - 1 // Signature ByteRange part 2 start byte directly starts after the actual signature. - context.ByteRangeValues[2] = context.ByteRangeValues[1] + int64(signatureMaxLength) + context.ByteRangeValues[2] = context.ByteRangeValues[1] + 1 + int64(signatureMaxLength) + 1 // Signature ByteRange part 2 length is everything else of the file. context.ByteRangeValues[3] = output_file_size - context.ByteRangeValues[2] diff --git a/sign/pdfcatalog.go b/sign/pdfcatalog.go index f027527..4f7a42f 100644 --- a/sign/pdfcatalog.go +++ b/sign/pdfcatalog.go @@ -30,7 +30,7 @@ func (context *SignContext) createCatalog() (catalog string, err error) { pages := root.Key("Pages").GetPtr() catalog += " /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R" catalog += " /AcroForm <<" - catalog += " /Fields [" + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R]" + catalog += " /Fields [" + strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R]" if !context.SignData.Signature.Approval { catalog += " /NeedAppearances false" @@ -44,12 +44,11 @@ func (context *SignContext) createCatalog() (catalog string, err error) { catalog += " >>" - // @todo: what do these do? if !context.SignData.Signature.Approval { if context.SignData.Signature.CertType > 0 { - //catalog += " /Perms << /DocMDP " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; + catalog += " /Perms << /DocMDP " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; } else { - //catalog += " /Perms << /UR3 " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; + catalog += " /Perms << /UR3 " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; } } diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 0f19ff4..9317333 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -29,7 +29,7 @@ func (context *SignContext) createSignaturePlaceholder() (signature string, byte // Create a placeholder for the actual signature content, we wil replace it later. signature += " /Contents<" + strings.Repeat("0", int(signatureMaxLength)) + ">" - if context.SignData.Signature.Approval { + if !context.SignData.Signature.Approval { signature += " /Reference [" // array of signature reference dictionaries signature += " << /Type /SigRef" if context.SignData.Signature.CertType > 0 { @@ -77,9 +77,6 @@ func (context *SignContext) createSignature() ([]byte, error) { io.Copy(sign_buf, context.OutputFile) file_content := sign_buf.Bytes() - // Remove trailing newline. - file_content = file_content[:len(file_content)-1] - // Collect the parts to sign. sign_content := make([]byte, 0) sign_content = append(sign_content, file_content[context.ByteRangeValues[0]:(context.ByteRangeValues[0]+context.ByteRangeValues[1])]...) @@ -111,7 +108,7 @@ func (context *SignContext) replaceSignature() error { dst := make([]byte, hex.EncodedLen(len(signature))) hex.Encode(dst, signature) - context.OutputFile.WriteAt(dst, context.ByteRangeValues[0]+context.ByteRangeValues[1]) + context.OutputFile.WriteAt(dst, context.ByteRangeValues[0] + context.ByteRangeValues[1] + 1) return nil } diff --git a/sign/pdftrailer.go b/sign/pdftrailer.go index 7332feb..ec06c18 100644 --- a/sign/pdftrailer.go +++ b/sign/pdftrailer.go @@ -19,7 +19,7 @@ func (context *SignContext) writeTrailer() error { new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R" size_string := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) - new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+2, 10) + new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+3, 10) trailer_string := string(trailer_buf) trailer_string = strings.Replace(trailer_string, root_string, new_root, -1) @@ -36,7 +36,7 @@ func (context *SignContext) writeTrailer() error { } // Write PDF ending. - if _, err := context.OutputFile.Write([]byte("%%EOF\n")); err != nil { + if _, err := context.OutputFile.Write([]byte("%%EOF")); err != nil { return err } diff --git a/sign/pdfvisualsignature.go b/sign/pdfvisualsignature.go new file mode 100644 index 0000000..b55296a --- /dev/null +++ b/sign/pdfvisualsignature.go @@ -0,0 +1,44 @@ +package sign + +import ( + "errors" + "strconv" +) + +func (context *SignContext) createVisualSignature() (visual_signature string, err error) { + visual_signature = strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 obj\n" + visual_signature += "<< /Type /Annot" + visual_signature += " /Subtype /Widget" + visual_signature += " /Rect [0 0 0 0]" + + root := context.PDFReader.Trailer().Key("Root") + root_keys := root.Keys() + found_pages := false + for _, key := range root_keys { + if key == "Pages" { + found_pages = true + break + } + } + + if !found_pages { + return "", errors.New("Didn't find pages in PDF trailer Root.") + } + + rootPtr := root.GetPtr() + context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R" + + page := root.Key("Pages").Key("Kids").Index(0).GetPtr() + visual_signature += " /P " + strconv.Itoa(int(page.GetID())) + " " + strconv.Itoa(int(page.GetGen())) + " R" + + visual_signature += " /F 4" + visual_signature += " /FT /Sig" + visual_signature += " /T " + pdfString("Signature") + visual_signature += " /Ff 0" + visual_signature += " /V " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R" + + visual_signature += " >>" + visual_signature += "\nendobj\n" + + return visual_signature, nil +} diff --git a/sign/pdfxref.go b/sign/pdfxref.go index cf2c66f..7c8fd1e 100644 --- a/sign/pdfxref.go +++ b/sign/pdfxref.go @@ -22,7 +22,7 @@ func (context *SignContext) writeXref() error { func (context *SignContext) writeXrefTable() error { xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) - new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+2, 10) + new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+3, 10) if _, err := context.OutputFile.Write([]byte(new_xref_size)); err != nil { return err @@ -34,7 +34,16 @@ func (context *SignContext) writeXrefTable() error { } // Create the new catalog xref line. - catalog_object_start_position := strconv.FormatInt(context.PDFReader.XrefInformation.StartPos, 10) + visual_signature_object_start_position := strconv.FormatInt(context.Filesize, 10) + visual_signature_xref_line := leftPad(visual_signature_object_start_position, "0", 10-len(visual_signature_object_start_position)) + " 00000 n\n" + + // Write the new catalog xref line. + if _, err := context.OutputFile.Write([]byte(visual_signature_xref_line)); err != nil { + return err + } + + // Create the new catalog xref line. + catalog_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length, 10) catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n\n" // Write the new catalog xref line. @@ -43,7 +52,7 @@ func (context *SignContext) writeXrefTable() error { } // Create the new signature xref line. - signature_object_start_position := strconv.FormatInt(context.PDFReader.XrefInformation.StartPos+context.CatalogData.Length, 10) + signature_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length, 10) signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n\n" // Write the new signature xref line. diff --git a/sign/sign.go b/sign/sign.go index 51b4c8a..fba1e71 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -7,6 +7,7 @@ import ( "time" "bitbucket.org/digitorus/pdf" + "io" ) type CatalogData struct { @@ -23,6 +24,11 @@ type SignData struct { CertificateChain []*x509.Certificate } +type VisualSignData struct { + ObjectId uint32 + Length int64 +} + type SignDataSignature struct { Approval bool CertType uint32 @@ -38,10 +44,12 @@ type SignDataSignatureInfo struct { } type SignContext struct { + Filesize int64 InputFile *os.File OutputFile *os.File SignData SignData CatalogData CatalogData + VisualSignData VisualSignData PDFReader *pdf.Reader NewXrefStart int64 ByteRangeStartByte int64 @@ -73,15 +81,20 @@ func SignFile(input string, output string, sign_data SignData) error { return err } - sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 1 + sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 2 + // We do size+1 because we insert a newline. context := SignContext{ + Filesize: size + 1, PDFReader: rdr, InputFile: input_file, OutputFile: output_file, - CatalogData: CatalogData{ + VisualSignData: VisualSignData{ ObjectId: uint32(rdr.XrefInformation.ItemCount), }, + CatalogData: CatalogData{ + ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1, + }, SignData: sign_data, } @@ -94,8 +107,30 @@ func SignFile(input string, output string, sign_data SignData) error { } func (context *SignContext) SignPDF() error { - // Write the PDF file to the output up til the xref. - if err := writePartFromSourceFileToTargetFile(context.InputFile, context.OutputFile, 0, context.PDFReader.XrefInformation.StartPos); err != nil { + // Copy old file into new file. + if _, err := io.Copy(context.OutputFile, context.InputFile); err != nil { + return err + } + + err := context.OutputFile.Sync() + if err != nil { + return err + } + + // File always needs an empty line after %%EOF. + if _, err := context.OutputFile.Write([]byte("\n")); err != nil { + return err + } + + visual_signature, err := context.createVisualSignature() + if err != nil { + return err + } + + context.VisualSignData.Length = int64(len(visual_signature)) + + // Write the new catalog object. + if _, err := context.OutputFile.Write([]byte(visual_signature)); err != nil { return err } @@ -115,8 +150,8 @@ func (context *SignContext) SignPDF() error { signature_object, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder() // Positions are relative to old start position of xref table. - byte_range_start_byte += context.PDFReader.XrefInformation.StartPos + int64(len(catalog)) - signature_contents_start_byte += context.PDFReader.XrefInformation.StartPos + int64(len(catalog)) + byte_range_start_byte += context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) + signature_contents_start_byte += context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) context.ByteRangeStartByte = byte_range_start_byte context.SignatureContentsStartByte = signature_contents_start_byte @@ -127,7 +162,7 @@ func (context *SignContext) SignPDF() error { } // Calculate the new start position of the xref table. - context.NewXrefStart = context.PDFReader.XrefInformation.StartPos + int64(len(signature_object)) + int64(len(catalog)) + context.NewXrefStart = context.Filesize + int64(len(signature_object)) + int64(len(catalog)) + int64(len(visual_signature)) if err := context.writeXref(); err != nil { return err @@ -145,5 +180,10 @@ func (context *SignContext) SignPDF() error { return err } + err = context.OutputFile.Sync() + if err != nil { + return err + } + return nil } diff --git a/verify/verify.go b/verify/verify.go index 5533e17..a993865 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -14,6 +14,7 @@ import ( "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" "golang.org/x/crypto/ocsp" + "go/src/log" ) type RevocationInfoArchival struct { @@ -70,9 +71,11 @@ func Verify(file *os.File) (apiResp *Response, err error) { finfo, _ := file.Stat() size := finfo.Size() + file.Seek(0, 0) + rdr, err := pdf.NewReader(file, size) if err != nil { - return nil, fmt.Errorf("Failed to open file") + return nil, fmt.Errorf("Failed to open file: %v", err) } // AcroForm will contain a SigFlags value if the form contains a digital signature @@ -129,6 +132,12 @@ func Verify(file *os.File) (apiResp *Response, err error) { apiResp.Error = fmt.Sprintln("Failed to get ByteRange:", i, err) } + log.Println(v.Key("ByteRange").Index(i-1).Int64()) + log.Println(v.Key("ByteRange").Index(i).Int64()) + log.Println(string(content[0:60])) + log.Println(string(content[len(content)-60:len(content)])) + log.Println(len(content)) + p7.Content = append(p7.Content, content...) } @@ -180,8 +189,10 @@ func Verify(file *os.File) (apiResp *Response, err error) { signer.ValidSignature = true signer.TrustedIssuer = false } + log.Println("Invalid sig") apiResp.Error = fmt.Sprintln("Failed to verify signature:", err) } else { + log.Println("Valid sig") signer.ValidSignature = true signer.TrustedIssuer = true } From 7b19471330f103fb399bf96c8dda871d49e1bc96 Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Tue, 11 Jul 2017 08:21:18 +0200 Subject: [PATCH 05/16] Valid signature --- sign/helpers.go | 16 ++++++++++++++++ sign/pdfcatalog.go | 4 ++-- sign/pdfinfo.go | 25 +++++++++++++++++++++++++ sign/pdfsignature.go | 2 +- sign/pdftrailer.go | 9 ++++++++- sign/pdfvisualsignature.go | 12 +++++++++--- sign/pdfxref.go | 19 ++++++++++++++----- sign/sign.go | 33 ++++++++++++++++++++++++++++----- verify/verify.go | 8 +------- 9 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 sign/pdfinfo.go diff --git a/sign/helpers.go b/sign/helpers.go index fb1c22d..91472a2 100644 --- a/sign/helpers.go +++ b/sign/helpers.go @@ -1,6 +1,8 @@ package sign import ( + "bitbucket.org/digitorus/pdf" + "errors" "fmt" "io" "math" @@ -9,6 +11,20 @@ import ( "time" ) +func findFirstPage(parent pdf.Value) (pdf.Value, error) { + value_type := parent.Key("Type").String() + if value_type == "/Pages" { + recurse_parent, recurse_err := findFirstPage(parent.Key("Kids").Index(0)) + return recurse_parent, recurse_err + } + + if value_type == "/Page" { + return parent, nil + } + + return parent, errors.New("Could not find first page.") +} + func pdfString(text string) string { text = strings.Replace(text, "\\", "\\\\", -1) text = strings.Replace(text, ")", "\\)", -1) diff --git a/sign/pdfcatalog.go b/sign/pdfcatalog.go index 4f7a42f..b8957b8 100644 --- a/sign/pdfcatalog.go +++ b/sign/pdfcatalog.go @@ -46,9 +46,9 @@ func (context *SignContext) createCatalog() (catalog string, err error) { if !context.SignData.Signature.Approval { if context.SignData.Signature.CertType > 0 { - catalog += " /Perms << /DocMDP " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; + catalog += " /Perms << /DocMDP " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>" } else { - catalog += " /Perms << /UR3 " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>"; + catalog += " /Perms << /UR3 " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>" } } diff --git a/sign/pdfinfo.go b/sign/pdfinfo.go new file mode 100644 index 0000000..d6661ff --- /dev/null +++ b/sign/pdfinfo.go @@ -0,0 +1,25 @@ +package sign + +import ( + "strconv" +) + +func (context *SignContext) createInfo() (info string, err error) { + original_info := context.PDFReader.Trailer().Key("Info") + info = strconv.Itoa(int(context.InfoData.ObjectId)) + " 0 obj\n" + info += "<<" + + info_keys := original_info.Keys() + for _, key := range info_keys { + info += "/" + key + if key == "ModDate" { + info += pdfDateTime(context.SignData.Signature.Info.Date) + } else { + info += pdfString(original_info.Key(key).RawString()) + } + } + + info += ">>" + info += "\nendobj\n" + return info, nil +} diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 9317333..956bd2e 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -108,7 +108,7 @@ func (context *SignContext) replaceSignature() error { dst := make([]byte, hex.EncodedLen(len(signature))) hex.Encode(dst, signature) - context.OutputFile.WriteAt(dst, context.ByteRangeValues[0] + context.ByteRangeValues[1] + 1) + context.OutputFile.WriteAt(dst, context.ByteRangeValues[0]+context.ByteRangeValues[1]+1) return nil } diff --git a/sign/pdftrailer.go b/sign/pdftrailer.go index ec06c18..e73b792 100644 --- a/sign/pdftrailer.go +++ b/sign/pdftrailer.go @@ -19,11 +19,18 @@ func (context *SignContext) writeTrailer() error { new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R" size_string := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) - new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+3, 10) + new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10) + + info := context.PDFReader.Trailer().Key("Info") + infoPtr := info.GetPtr() + + info_string := "Info " + strconv.Itoa(int(infoPtr.GetID())) + " " + strconv.Itoa(int(infoPtr.GetGen())) + " R" + new_info := "Info " + strconv.FormatInt(int64(context.InfoData.ObjectId), 10) + " 0 R" trailer_string := string(trailer_buf) trailer_string = strings.Replace(trailer_string, root_string, new_root, -1) trailer_string = strings.Replace(trailer_string, size_string, new_size, -1) + trailer_string = strings.Replace(trailer_string, info_string, new_info, -1) // Write the new trailer. if _, err := context.OutputFile.Write([]byte(trailer_string)); err != nil { diff --git a/sign/pdfvisualsignature.go b/sign/pdfvisualsignature.go index b55296a..bbc9007 100644 --- a/sign/pdfvisualsignature.go +++ b/sign/pdfvisualsignature.go @@ -28,10 +28,16 @@ func (context *SignContext) createVisualSignature() (visual_signature string, er rootPtr := root.GetPtr() context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R" - page := root.Key("Pages").Key("Kids").Index(0).GetPtr() - visual_signature += " /P " + strconv.Itoa(int(page.GetID())) + " " + strconv.Itoa(int(page.GetGen())) + " R" + first_page, err := findFirstPage(root.Key("Pages")) + if err != nil { + return "", err + } - visual_signature += " /F 4" + first_page_ptr := first_page.GetPtr() + + visual_signature += " /P " + strconv.Itoa(int(first_page_ptr.GetID())) + " " + strconv.Itoa(int(first_page_ptr.GetGen())) + " R" + + visual_signature += " /F 132" visual_signature += " /FT /Sig" visual_signature += " /T " + pdfString("Signature") visual_signature += " /Ff 0" diff --git a/sign/pdfxref.go b/sign/pdfxref.go index 7c8fd1e..566e6cb 100644 --- a/sign/pdfxref.go +++ b/sign/pdfxref.go @@ -22,7 +22,7 @@ func (context *SignContext) writeXref() error { func (context *SignContext) writeXrefTable() error { xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) - new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+3, 10) + new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10) if _, err := context.OutputFile.Write([]byte(new_xref_size)); err != nil { return err @@ -35,7 +35,7 @@ func (context *SignContext) writeXrefTable() error { // Create the new catalog xref line. visual_signature_object_start_position := strconv.FormatInt(context.Filesize, 10) - visual_signature_xref_line := leftPad(visual_signature_object_start_position, "0", 10-len(visual_signature_object_start_position)) + " 00000 n\n" + visual_signature_xref_line := leftPad(visual_signature_object_start_position, "0", 10-len(visual_signature_object_start_position)) + " 00000 n \n" // Write the new catalog xref line. if _, err := context.OutputFile.Write([]byte(visual_signature_xref_line)); err != nil { @@ -44,7 +44,7 @@ func (context *SignContext) writeXrefTable() error { // Create the new catalog xref line. catalog_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length, 10) - catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n\n" + catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n \n" // Write the new catalog xref line. if _, err := context.OutputFile.Write([]byte(catalog_xref_line)); err != nil { @@ -52,8 +52,17 @@ func (context *SignContext) writeXrefTable() error { } // Create the new signature xref line. - signature_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length, 10) - signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n\n" + info_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length, 10) + info_xref_line := leftPad(info_object_start_position, "0", 10-len(info_object_start_position)) + " 00000 n \n" + + // Write the new signature xref line. + if _, err := context.OutputFile.Write([]byte(info_xref_line)); err != nil { + return err + } + + // Create the new signature xref line. + signature_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length+context.InfoData.Length, 10) + signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n \n" // Write the new signature xref line. if _, err := context.OutputFile.Write([]byte(signature_xref_line)); err != nil { diff --git a/sign/sign.go b/sign/sign.go index fba1e71..e8c1d30 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -3,11 +3,11 @@ package sign import ( "crypto" "crypto/x509" + "io" "os" "time" "bitbucket.org/digitorus/pdf" - "io" ) type CatalogData struct { @@ -29,6 +29,11 @@ type VisualSignData struct { Length int64 } +type InfoData struct { + ObjectId uint32 + Length int64 +} + type SignDataSignature struct { Approval bool CertType uint32 @@ -50,6 +55,7 @@ type SignContext struct { SignData SignData CatalogData CatalogData VisualSignData VisualSignData + InfoData InfoData PDFReader *pdf.Reader NewXrefStart int64 ByteRangeStartByte int64 @@ -81,7 +87,7 @@ func SignFile(input string, output string, sign_data SignData) error { return err } - sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 2 + sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 // We do size+1 because we insert a newline. context := SignContext{ @@ -95,6 +101,9 @@ func SignFile(input string, output string, sign_data SignData) error { CatalogData: CatalogData{ ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1, }, + InfoData: InfoData{ + ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2, + }, SignData: sign_data, } @@ -149,9 +158,23 @@ func (context *SignContext) SignPDF() error { // Create the signature object signature_object, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder() + info, err := context.createInfo() + if err != nil { + return err + } + + context.InfoData.Length = int64(len(info)) + + // Write the new catalog object. + if _, err := context.OutputFile.Write([]byte(info)); err != nil { + return err + } + + appended_bytes := context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) + int64(len(info)) + // Positions are relative to old start position of xref table. - byte_range_start_byte += context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) - signature_contents_start_byte += context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) + byte_range_start_byte += appended_bytes + signature_contents_start_byte += appended_bytes context.ByteRangeStartByte = byte_range_start_byte context.SignatureContentsStartByte = signature_contents_start_byte @@ -162,7 +185,7 @@ func (context *SignContext) SignPDF() error { } // Calculate the new start position of the xref table. - context.NewXrefStart = context.Filesize + int64(len(signature_object)) + int64(len(catalog)) + int64(len(visual_signature)) + context.NewXrefStart = appended_bytes + int64(len(signature_object)) if err := context.writeXref(); err != nil { return err diff --git a/verify/verify.go b/verify/verify.go index a993865..74b9743 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -13,8 +13,8 @@ import ( "bitbucket.org/digitorus/pdf" "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" - "golang.org/x/crypto/ocsp" "go/src/log" + "golang.org/x/crypto/ocsp" ) type RevocationInfoArchival struct { @@ -132,12 +132,6 @@ func Verify(file *os.File) (apiResp *Response, err error) { apiResp.Error = fmt.Sprintln("Failed to get ByteRange:", i, err) } - log.Println(v.Key("ByteRange").Index(i-1).Int64()) - log.Println(v.Key("ByteRange").Index(i).Int64()) - log.Println(string(content[0:60])) - log.Println(string(content[len(content)-60:len(content)])) - log.Println(len(content)) - p7.Content = append(p7.Content, content...) } From 099b765c01e182db6c652e2ec724a87c55bf467d Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Tue, 11 Jul 2017 20:25:15 +0200 Subject: [PATCH 06/16] Add TSA --- sign.go | 3 ++ sign/pdfsignature.go | 102 ++++++++++++++++++++++++++++++++++++++++++- sign/sign.go | 7 +++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/sign.go b/sign.go index 698bfb6..057dc45 100644 --- a/sign.go +++ b/sign.go @@ -103,6 +103,9 @@ func main() { }, Signer: pkey, Certificate: cert, + TSA: sign.TSA{ + URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", + }, }) if err != nil { log.Println(err) diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 956bd2e..d2a93cb 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -2,14 +2,32 @@ package sign import ( "bytes" + "encoding/asn1" "encoding/hex" + "errors" + "fmt" "io" + "io/ioutil" + "net/http" "strconv" "strings" "github.com/digitorus/pkcs7" + "github.com/digitorus/timestamp" ) +type pkiStatusInfo struct { + Status int + StatusString string `asn1:"optional"` + FailInfo int `asn1:"optional"` +} + +// 2.4.2. Response Format +type TSAResponse struct { + Status pkiStatusInfo + TimeStampToken asn1.RawValue +} + var signatureMaxLength = uint32(11742) var signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]" @@ -88,8 +106,41 @@ func (context *SignContext) createSignature() ([]byte, error) { return nil, err } + signer_config := pkcs7.SignerInfoConfig{} + + if context.SignData.TSA.URL != "" { + timestamp_response, err := context.GetTSA(sign_content) + if err != nil { + return nil, err + } + + var rest []byte + var resp TSAResponse + if rest, err = asn1.Unmarshal(timestamp_response, &resp); err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, errors.New("trailing data in Time-Stamp response") + } + + if resp.Status.Status > 0 { + return nil, errors.New(fmt.Sprintf("%s: %s", timestamp.FailureInfo(resp.Status.FailInfo).String(), resp.Status.StatusString)) + } + + if len(resp.TimeStampToken.Bytes) == 0 { + return nil, errors.New("no pkcs7 data in Time-Stamp response") + } + + timestamp_attribute := pkcs7.Attribute{ + Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}, + Value: resp.TimeStampToken, + } + signer_config.ExtraUnsignedAttributes = append(signer_config.ExtraSignedAttributes, timestamp_attribute) + } + // Add the signer and sign the data. - if err := signed_data.AddSignerChain(context.SignData.Certificate, context.SignData.Signer, context.SignData.CertificateChain, pkcs7.SignerInfoConfig{}); err != nil { + if err := signed_data.AddSignerChain(context.SignData.Certificate, context.SignData.Signer, context.SignData.CertificateChain, signer_config); err != nil { + return nil, err } @@ -99,6 +150,55 @@ func (context *SignContext) createSignature() ([]byte, error) { return signed_data.Finish() } +func (context *SignContext) GetTSA(sign_content []byte) (timestamp_response []byte, err error) { + sign_reader := bytes.NewReader(sign_content) + ts_request, err := timestamp.CreateRequest(sign_reader, nil) + if err != nil { + return nil, err + } + + ts_request_reader := bytes.NewReader(ts_request) + req, err := http.NewRequest("POST", context.SignData.TSA.URL, ts_request_reader) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/timestamp-query") + req.Header.Add("Content-Transfer-Encoding", "binary") + + if context.SignData.TSA.Username != "" && context.SignData.TSA.Password != "" { + req.SetBasicAuth(context.SignData.TSA.Username, context.SignData.TSA.Password) + } + + client := &http.Client{} + resp, err := client.Do(req) + code := 0 + + if resp != nil { + code = resp.StatusCode + } + + if err != nil || (code < 200 || code > 299) { + if err == nil { + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + err = errors.New("Non success response (" + strconv.Itoa(code) + "): " + string(body)) + } else { + err = errors.New("Non success response (" + strconv.Itoa(code) + ")") + } + + return nil, err + } + + defer resp.Body.Close() + timestamp_response_body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return timestamp_response_body, nil +} + func (context *SignContext) replaceSignature() error { signature, err := context.createSignature() if err != nil { diff --git a/sign/sign.go b/sign/sign.go index e8c1d30..b92b7e1 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -16,12 +16,19 @@ type CatalogData struct { RootString string } +type TSA struct { + URL string + Username string + Password string +} + type SignData struct { ObjectId uint32 Signature SignDataSignature Signer crypto.Signer Certificate *x509.Certificate CertificateChain []*x509.Certificate + TSA TSA } type VisualSignData struct { From c327f686d5605dade7885a162e4e30a017d4e5b1 Mon Sep 17 00:00:00 2001 From: Paul van Brouwershaven Date: Wed, 12 Jul 2017 20:53:06 +0200 Subject: [PATCH 07/16] Added a dedicated revocation subpackage The newly created dedicated revocation package is used to encode and decode revocation information. Signing and verification can now use the same structures. While the InfoArchival stucsture can now be created this stucture is currenlty not embedded into the document. Test cases for obtaining and embedding revocation information into the InfoArchival struct are also availible. --- revocation/revocation.go | 55 ++++++++++++++++++ sign/revocation.go | 88 +++++++++++++++++++++++++++++ sign/revocation_test.go | 119 +++++++++++++++++++++++++++++++++++++++ verify/verify.go | 19 +------ 4 files changed, 265 insertions(+), 16 deletions(-) create mode 100644 revocation/revocation.go create mode 100644 sign/revocation.go create mode 100644 sign/revocation_test.go diff --git a/revocation/revocation.go b/revocation/revocation.go new file mode 100644 index 0000000..fcab598 --- /dev/null +++ b/revocation/revocation.go @@ -0,0 +1,55 @@ +package revocation + +import ( + "encoding/asn1" + "crypto/x509" +) + +// InfoArchival is the pkcs7 container containing the revocation information for +// all embedded certificates. +// +// Currently the internal structure is exposed but I don't like to expose the +// asn1.RawValue objects. We can probably make them private and expose the +// information with functions. +type InfoArchival struct { + CRL CRL `asn1:"tag:0,optional,explicit"` + OCSP OCSP `asn1:"tag:1,optional,explicit"` + Other Other `asn1:"tag:2,optional,explicit"` +} + +// AddCRL is used to embed an CRL to revocation.InfoArchival object. You directly +// pass the bytes of a downloaded CRL to this function. +func (r *InfoArchival) AddCRL(b []byte) error { + r.CRL = append(r.CRL, asn1.RawValue{FullBytes: b}) + return nil +} + +// AddOCSP is used to embed the raw bytes of an OCSP response. +func (r *InfoArchival) AddOCSP(b []byte) error { + r.OCSP = append(r.OCSP, asn1.RawValue{FullBytes: b}) + return nil +} + +// IsRevoked checks if there is a status inclded for the certificate and returns +// true if the certificate is marked as revoked. +// +// TODO: We should report if there is no CRL or OCSP response embeded for this certificate +// TODO: Information about the revocation (time, reason, etc) must be extractable +func (r *InfoArchival) IsRevoked(c *x509.Certificate) bool { + // check the crl and ocsp to see if this certificate is revoked + return true +} + +// CRL contains the raw bytes of a pkix.CertificateList and can be parsed with +// x509.PParseCRL. +type CRL []asn1.RawValue + +// OCSP contains the raw bytes of an OCSP response and can be parsed with +// x/crypto/ocsp.ParseResponse +type OCSP []asn1.RawValue + +// ANS.1 Object OtherRevInfo +type Other struct { + Type asn1.ObjectIdentifier + Value []byte +} diff --git a/sign/revocation.go b/sign/revocation.go new file mode 100644 index 0000000..5796e62 --- /dev/null +++ b/sign/revocation.go @@ -0,0 +1,88 @@ +package sign + +import ( + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "bitbucket.org/digitorus/pdfsign/revocation" + + "golang.org/x/crypto/ocsp" +) + +func embedOCSPRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { + req, err := ocsp.CreateRequest(cert, issuer, nil) + if err != nil { + return err + } + + ocspUrl := fmt.Sprintf("%s/%s", strings.TrimRight(cert.OCSPServer[0], "/"), + base64.StdEncoding.EncodeToString(req)) + resp, err := http.Get(ocspUrl) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // check if we got a valid OCSP response + _, err = ocsp.ParseResponseForCert(body, cert, issuer) + if err != nil { + return err + } + + i.AddOCSP(body) + return nil +} + +// embedCRLRevocationStatus requires an issuer as it needs to implement the +// the interface, a nil argment might be given if the issuer is not known. +func embedCRLRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { + resp, err := http.Get(cert.CRLDistributionPoints[0]) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // TODO: verify crl and certificate before embedding + i.AddCRL(body) + return nil +} + +func embedRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { + // For each certificate a revoction status needs to be included, this can be done + // by embedding a CRL or OCSP response. In most cases an OCSP response is smaller + // to embed in the document but and empty CRL (often seen of dediced high volume + // hirachies) can be smaller. + // + // There have been some reports that the usage of a CRL would result in a better + // compatibilty. + // + // TODO: Find and embed link about compatibilty + // TODO: Implement revocation status caching (required for higher volume signing) + + // using an OCSP server + if len(cert.OCSPServer) > 0 { + embedOCSPRevocationStatus(cert, issuer, i) + return nil + } + + // using a crl + if len(cert.CRLDistributionPoints) > 0 { + embedCRLRevocationStatus(cert, issuer, i) + return nil + } + + return errors.New("certificate contains no information to check status") +} diff --git a/sign/revocation_test.go b/sign/revocation_test.go new file mode 100644 index 0000000..34b79b0 --- /dev/null +++ b/sign/revocation_test.go @@ -0,0 +1,119 @@ +package sign + +import ( + "crypto/x509" + "encoding/pem" + "testing" + + "bitbucket.org/digitorus/pdfsign/revocation" +) + +const certPem = `-----BEGIN CERTIFICATE----- +MIIGKDCCBRCgAwIBAgIMW8J4m7huCPO5f+wbMA0GCSqGSIb3DQEBCwUAMEsxCzAJ +BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSEwHwYDVQQDExhH +bG9iYWxTaWduIENBIDIgZm9yIEFBVEwwHhcNMTcwNzA2MDk0MDUyWhcNMjAwNzA2 +MDk0MDUyWjCBpzELMAkGA1UEBhMCR0IxDTALBgNVBAgTBEtlbnQxEjAQBgNVBAcT +CU1haWRzdG9uZTEfMB0GA1UEChMWR01PIEdsb2JhbFNpZ24gTGltaXRlZDEfMB0G +A1UEAxMWUGF1bCBWYW4gQnJvdXdlcnNoYXZlbjEzMDEGCSqGSIb3DQEJARYkcGF1 +bC52YW5icm91d2Vyc2hhdmVuQGdsb2JhbHNpZ24uY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAr5jbAIZDjkWngxlwJqneE9VEDTvmMIGwvgy71g5j +k+igHxB6tTfaqGD87oIm2wcrlZHpPJG9n2Rh9FhFmvx8ZXiceNI9Ks5Ho5iYNFUk +y2JuVfFPxtp6amqpLzM5HZUePgu1Gdy1Zn1PUajii7paFPuhdemcA9DdTAQ1GsDv +C9MZ2D5sKM0hLCRePCzJ3TeQmHefFrC0XQ7u2i7LDD990URiFz7WNq2tSDJwBe/6 +1tMewpekmtE5X43PqzgcyGBsDJqAKcthsLQnhqrIryuwE2bEhP/FxQrHkT+f/OkK +vwAjB9gYgJRNoMFUwXLW789JsOkbCqepoWsg1PnWrAbAMQIDAQABo4ICrTCCAqkw +DgYDVR0PAQH/BAQDAgeAMIGIBggrBgEFBQcBAQR8MHowQQYIKwYBBQUHMAKGNWh0 +dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2dzYWF0bDJzaGEyZzIu +Y3J0MDUGCCsGAQUFBzABhilodHRwOi8vb2NzcDIuZ2xvYmFsc2lnbi5jb20vZ3Nh +YXRsMnNoYTJnMjCB2wYDVR0gBIHTMIHQMIHNBgsrBgEEAaAyASgeAjCBvTCBhgYI +KwYBBQUHAgIwegx4VGhpcyBjZXJ0aWZpY2F0ZSBoYXMgYmVlbiBpc3N1ZWQgaW4g +YWNjb3JkYW5jZSB3aXRoIHRoZSBHbG9iYWxTaWduIENQUyBsb2NhdGVkIGF0IGh0 +dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMDIGCCsGAQUFBwIB +FiZodHRwczovL3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5LzAJBgNVHRME +AjAAMD8GA1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20v +Z3MvZ3NhYXRsMnNoYTJnMi5jcmwwLwYDVR0RBCgwJoEkcGF1bC52YW5icm91d2Vy +c2hhdmVuQGdsb2JhbHNpZ24uY29tMFwGCiqGSIb3LwEBCQEETjBMAgEBhkRodHRw +Oi8vYWF0bC10aW1lc3RhbXAuZ2xvYmFsc2lnbi5jb20vdHNhL2FvaGZld2F0MjM4 +OTUzNWZuYXNnbmxnNW0yMwEBADATBgNVHSUEDDAKBggrBgEFBQcDBDAdBgNVHQ4E +FgQUtimjVds00OgBZ747tqkKbZJeCCowHwYDVR0jBBgwFoAUxRNOzofGiRsj6EDj +dTKbA3A67+8wDQYJKoZIhvcNAQELBQADggEBACBUFVwUKVpOWt1eLf7lKKPfVhEL +9QrkAkV/UZPMsDwBDIJhphqjCqfbJVTgybm79gUCJiwbarCYOHRgFAdNTPEvEcT0 ++XwR6WZcDdfQAtaHfO6X9ExgJv93txoFVcpYLY1hR3o6QdP4VQSDhRTv3bM1j/WC +mcCoiIQz28Y8L+8rRx5J7JAgYpupoU/8sCpidBMhYAGF5p8Z8p0LbqvZndRHaVqp +yXQ0kYj1n45it5FXsKECWZKTx0v4IBySJY3RGpF+5cpPUYulJfINBg7nj7aQG/Uv +qtyxnVAG3W4pTHWd/0Gyc3lrgRtyZy+b9DaxHZ/N6HHNgnRHB4PUkektpX4= +-----END CERTIFICATE-----` + +const issuerPem = `-----BEGIN CERTIFICATE----- +MIIEpjCCA46gAwIBAgIORea3r9ymcb22XRTz2sAwDQYJKoZIhvcNAQELBQAwVzEL +MAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExLTArBgNVBAMT +JEdsb2JhbFNpZ24gQ0EgZm9yIEFBVEwgLSBTSEEyNTYgLSBHMjAeFw0xNDEyMTAw +MDAwMDBaFw0yNDEyMTAwMDAwMDBaMEsxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBH +bG9iYWxTaWduIG52LXNhMSEwHwYDVQQDExhHbG9iYWxTaWduIENBIDIgZm9yIEFB +VEwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJS4vhNfaSmXtX7bWy +hVhBGqisbqgnX9/K5psHMrkwm10pnicCSmvb6nvgMAPbRPyfpGHj5ArrDli6rCDR +CLR1tjied/6AxQCCPgyvDyEwDWxzytVHkCldzEHjHmc1kL0zI7aQfNrD25xAUnHa +X2jBm71filgduyQBfuLJLlL/NGh46X6eI9xpqBmqwBFa6wHPisnwkAB22CQB/OY3 +mnlhLCJmrEL9fGPRDQnc6w849ws3nkKISkEPmTyfUAJUkEgCxOjfiwoNSFjZ99lA +K+ujj90dqYU3mJ7dTEHA2+dFrdvVsoyA4JZBu7utxe2rJgpifO3B/L+f4Cat12kA +2IqtAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB +/wIBADAdBgNVHQ4EFgQUxRNOzofGiRsj6EDjdTKbA3A67+8wHwYDVR0jBBgwFoAU +YMLxUj6tjBPc28oO+grmKiyZS9gwgYYGCCsGAQUFBwEBBHoweDA0BggrBgEFBQcw +AYYoaHR0cDovL29jc3AyLmdsb2JhbHNpZ24uY29tL2dzYWF0bHNoYTJnMjBABggr +BgEFBQcwAoY0aHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9jYWNlcnQvZ3Nh +YXRsc2hhMmcyLmNydDA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY3JsLmdsb2Jh +bHNpZ24uY29tL2dzL2dzYWF0bHNoYTJnMi5jcmwwRwYDVR0gBEAwPjA8BgRVHSAA +MDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z +aXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQAkH9vI0bJPzbtf5ikdFb5Kc4L9/IIZ +GPeVbS3UPl8x4WPzmZHwGvSeG53DDcJ9weGHO4PORBgBT5KDZEt7CKXd4vt83xw8 +P1kYvvjv4E+8R8VD3hP48zkygHMR3JtBdMPkbNbE10TCb4XmQPpkwGIVhaD7ojtS ++4mPjVts6ZZzbnHI42CbYwdaOf2W8IUu0b1w4T7T5YPfi8rSKwQxIKibFG1mSsOC +vG9tDxJVJUNdiWTPoHGn+n+3qeJvPRgHhicZ+ivOqqmLQSNgtp1WdQ+uJmUqU7EY +htN+laG7bS/8xGTPothL9Abgd/9L3X0KKGUDCdcpzRuy20CI7E4uygD8 +-----END CERTIFICATE-----` + +func TestEmbedRevocationStatus(t *testing.T) { + var ia revocation.InfoArchival + + err := embedRevocationStatus(pemToCert(certPem), pemToCert(issuerPem), &ia) + if err != nil { + t.Errorf("%s", err.Error()) + } + + if len(ia.OCSP) != 1 { + t.Errorf("Expected one OCSP status") + } +} + +func TestEmbedOCSPRevocationStatus(t *testing.T) { + var ia revocation.InfoArchival + + err := embedOCSPRevocationStatus(pemToCert(certPem), pemToCert(issuerPem), &ia) + if err != nil { + t.Errorf("%s", err.Error()) + } + + if len(ia.OCSP) != 1 { + t.Errorf("Expected one OCSP status") + } +} + +func TestEmbedCRLRevocationStatus(t *testing.T) { + var ia revocation.InfoArchival + + err := embedCRLRevocationStatus(pemToCert(certPem), nil, &ia) + if err != nil { + t.Errorf("%s", err.Error()) + } + + if len(ia.CRL) != 1 { + t.Errorf("Expected one CRL") + } +} + +func pemToCert(p string) *x509.Certificate { + block, _ := pem.Decode([]byte(p)) + cert, _ := x509.ParseCertificate(block.Bytes) + + return cert +} diff --git a/verify/verify.go b/verify/verify.go index 74b9743..e713c8e 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -11,26 +11,13 @@ import ( "time" "bitbucket.org/digitorus/pdf" + "bitbucket.org/digitorus/pdfsign/revocation" "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" - "go/src/log" + "log" "golang.org/x/crypto/ocsp" ) -type RevocationInfoArchival struct { - CRL RevCRL `asn1:"tag:0,optional,explicit"` - OCSP RevOCSP `asn1:"tag:1,optional,explicit"` - OtherRevInfo OtherRevInfo `asn1:"tag:2,optional,explicit"` -} - -type RevCRL []asn1.RawValue -type RevOCSP []asn1.RawValue - -type OtherRevInfo struct { - Type asn1.ObjectIdentifier - Value []byte -} - type Response struct { Error string @@ -192,7 +179,7 @@ func Verify(file *os.File) (apiResp *Response, err error) { } // PDF signature certificate revocation information attribute (1.2.840.113583.1.1.8) - var revInfo RevocationInfoArchival + var revInfo revocation.InfoArchival p7.UnmarshalSignedAttribute(asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, &revInfo) // Parse OCSP response From c8e53c9bcf2d6c3a28f188daae8130b7e73cc4bc Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Thu, 13 Jul 2017 20:30:18 +0200 Subject: [PATCH 08/16] Add revocation information --- certificate.pem | 19 +++++++++++++++++++ revocation/revocation.go | 8 ++++---- sign.go | 5 ++++- sign/pdfsignature.go | 25 +++++++++++++++++++++++-- sign/revocation.go | 23 ++++++++++++----------- sign/revocation_test.go | 2 +- sign/sign.go | 23 ++++++++++++++--------- 7 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 certificate.pem diff --git a/certificate.pem b/certificate.pem new file mode 100644 index 0000000..ddeeb1f --- /dev/null +++ b/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBDCCAm2gAwIBAgIJAP6vkkLP72OOMA0GCSqGSIb3DQEBCwUAMIGZMQswCQYD +VQQGEwJOTDEVMBMGA1UECAwMWnVpZC1Ib2xsYW5kMRIwEAYDVQQHDAlSb3R0ZXJk +YW0xEjAQBgNVBAoMCVVuaWNvZGVyczELMAkGA1UECwwCSVQxGjAYBgNVBAMMEUpl +cm9lbiBCb2JiZWxkaWprMSIwIAYJKoZIhvcNAQkBFhNqZXJvZW5AdW5pY29kZXJz +Lm5sMCAXDTE3MDcwNjE5MzYwOVoYDzMwMTYxMTA2MTkzNjA5WjCBmTELMAkGA1UE +BhMCTkwxFTATBgNVBAgMDFp1aWQtSG9sbGFuZDESMBAGA1UEBwwJUm90dGVyZGFt +MRIwEAYDVQQKDAlVbmljb2RlcnMxCzAJBgNVBAsMAklUMRowGAYDVQQDDBFKZXJv +ZW4gQm9iYmVsZGlqazEiMCAGCSqGSIb3DQEJARYTamVyb2VuQHVuaWNvZGVycy5u +bDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArpfaVUltYdOSISuc8V5vAy6b +jpqYuxsS5I6jpL1nMKms9IB5+uk+Glo2O/tb+W/R8zxQ3xrQ6JWZ4ZSsBhKNVink +Su3+kdAQJfHn3NLJzx0QGceo0TF2RvVGo5c91zxuA8rchdNz1QxrD6QesGKyfsXn +F+oELezafT346PbeqikCAwEAAaNQME4wHQYDVR0OBBYEFKA68BB0iwhY2RIRFIYs +gmq0l6y7MB8GA1UdIwQYMBaAFKA68BB0iwhY2RIRFIYsgmq0l6y7MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADgYEAZ75HjcE/d/nclPTQbCN9qvUyuU76ml4O +jDN8T+loOsUKmI4VVsNLzF6DXq8sg4EP7s8kEEzM7qhoijw09OUhVniBYN3SzJYX +l8AiThPGqcIm1TrkqPULYQBu/FnMoL6SP7kAULcsUvEmn1rPcG9ESQ4sK/ceJhFZ +zk9o3rVC0PU= +-----END CERTIFICATE----- diff --git a/revocation/revocation.go b/revocation/revocation.go index fcab598..0c44148 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -1,8 +1,8 @@ package revocation import ( + "crypto/x509" "encoding/asn1" - "crypto/x509" ) // InfoArchival is the pkcs7 container containing the revocation information for @@ -21,13 +21,13 @@ type InfoArchival struct { // pass the bytes of a downloaded CRL to this function. func (r *InfoArchival) AddCRL(b []byte) error { r.CRL = append(r.CRL, asn1.RawValue{FullBytes: b}) - return nil + return nil } // AddOCSP is used to embed the raw bytes of an OCSP response. func (r *InfoArchival) AddOCSP(b []byte) error { r.OCSP = append(r.OCSP, asn1.RawValue{FullBytes: b}) - return nil + return nil } // IsRevoked checks if there is a status inclded for the certificate and returns @@ -37,7 +37,7 @@ func (r *InfoArchival) AddOCSP(b []byte) error { // TODO: Information about the revocation (time, reason, etc) must be extractable func (r *InfoArchival) IsRevoked(c *x509.Certificate) bool { // check the crl and ocsp to see if this certificate is revoked - return true + return true } // CRL contains the raw bytes of a pkix.CertificateList and can be parsed with diff --git a/sign.go b/sign.go index 057dc45..9e393f2 100644 --- a/sign.go +++ b/sign.go @@ -11,6 +11,7 @@ import ( "errors" "io/ioutil" + "bitbucket.org/digitorus/pdfsign/revocation" "bitbucket.org/digitorus/pdfsign/sign" "bitbucket.org/digitorus/pdfsign/verify" ) @@ -104,8 +105,10 @@ func main() { Signer: pkey, Certificate: cert, TSA: sign.TSA{ - URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", + URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", }, + RevocationData: revocation.InfoArchival{}, + RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, }) if err != nil { log.Println(err) diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index d2a93cb..00b5b8a 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -135,12 +135,33 @@ func (context *SignContext) createSignature() ([]byte, error) { Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}, Value: resp.TimeStampToken, } - signer_config.ExtraUnsignedAttributes = append(signer_config.ExtraSignedAttributes, timestamp_attribute) + signer_config.ExtraUnsignedAttributes = append(signer_config.ExtraUnsignedAttributes, timestamp_attribute) + } + + if context.SignData.RevocationFunction != nil { + err = context.SignData.RevocationFunction(context.SignData.Certificate, nil, &context.SignData.RevocationData) + if err != nil { + return nil, err + } + + if context.SignData.CertificateChain != nil && len(context.SignData.CertificateChain) > 0 { + for _, cert := range context.SignData.CertificateChain { + err = context.SignData.RevocationFunction(cert, nil, &context.SignData.RevocationData) + if err != nil { + return nil, err + } + } + } + + revocation_attribute := pkcs7.Attribute{ + Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, + Value: context.SignData.RevocationData, + } + signer_config.ExtraSignedAttributes = append(signer_config.ExtraSignedAttributes, revocation_attribute) } // Add the signer and sign the data. if err := signed_data.AddSignerChain(context.SignData.Certificate, context.SignData.Signer, context.SignData.CertificateChain, signer_config); err != nil { - return nil, err } diff --git a/sign/revocation.go b/sign/revocation.go index 5796e62..c969cb5 100644 --- a/sign/revocation.go +++ b/sign/revocation.go @@ -3,7 +3,6 @@ package sign import ( "crypto/x509" "encoding/base64" - "errors" "fmt" "io/ioutil" "net/http" @@ -38,8 +37,7 @@ func embedOCSPRevocationStatus(cert, issuer *x509.Certificate, i *revocation.Inf return err } - i.AddOCSP(body) - return nil + return i.AddOCSP(body) } // embedCRLRevocationStatus requires an issuer as it needs to implement the @@ -56,11 +54,10 @@ func embedCRLRevocationStatus(cert, issuer *x509.Certificate, i *revocation.Info } // TODO: verify crl and certificate before embedding - i.AddCRL(body) - return nil + return i.AddCRL(body) } -func embedRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { +func DefaultEmbedRevocationStatusFunction(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error { // For each certificate a revoction status needs to be included, this can be done // by embedding a CRL or OCSP response. In most cases an OCSP response is smaller // to embed in the document but and empty CRL (often seen of dediced high volume @@ -74,15 +71,19 @@ func embedRevocationStatus(cert, issuer *x509.Certificate, i *revocation.InfoArc // using an OCSP server if len(cert.OCSPServer) > 0 { - embedOCSPRevocationStatus(cert, issuer, i) - return nil + err := embedOCSPRevocationStatus(cert, issuer, i) + if err != nil { + return err + } } // using a crl if len(cert.CRLDistributionPoints) > 0 { - embedCRLRevocationStatus(cert, issuer, i) - return nil + err := embedCRLRevocationStatus(cert, issuer, i) + if err != nil { + return err + } } - return errors.New("certificate contains no information to check status") + return nil } diff --git a/sign/revocation_test.go b/sign/revocation_test.go index 34b79b0..7542aac 100644 --- a/sign/revocation_test.go +++ b/sign/revocation_test.go @@ -75,7 +75,7 @@ htN+laG7bS/8xGTPothL9Abgd/9L3X0KKGUDCdcpzRuy20CI7E4uygD8 func TestEmbedRevocationStatus(t *testing.T) { var ia revocation.InfoArchival - err := embedRevocationStatus(pemToCert(certPem), pemToCert(issuerPem), &ia) + err := DefaultEmbedRevocationStatusFunction(pemToCert(certPem), pemToCert(issuerPem), &ia) if err != nil { t.Errorf("%s", err.Error()) } diff --git a/sign/sign.go b/sign/sign.go index b92b7e1..3fe16a0 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -8,6 +8,7 @@ import ( "time" "bitbucket.org/digitorus/pdf" + "bitbucket.org/digitorus/pdfsign/revocation" ) type CatalogData struct { @@ -17,18 +18,22 @@ type CatalogData struct { } type TSA struct { - URL string - Username string - Password string + URL string + Username string + Password string } +type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error + type SignData struct { - ObjectId uint32 - Signature SignDataSignature - Signer crypto.Signer - Certificate *x509.Certificate - CertificateChain []*x509.Certificate - TSA TSA + ObjectId uint32 + Signature SignDataSignature + Signer crypto.Signer + Certificate *x509.Certificate + CertificateChain []*x509.Certificate + TSA TSA + RevocationData revocation.InfoArchival + RevocationFunction RevocationFunction } type VisualSignData struct { From 3928b7d8137b16636552ccd669152033e793b325 Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Thu, 13 Jul 2017 21:55:35 +0200 Subject: [PATCH 09/16] Creation of chain, add chain to signing, fetch OCSP/CRL for chain --- sign.go | 30 +++++++++++++++++++++++++++++- sign/pdfsignature.go | 36 ++++++++++++++++++++++++------------ sign/revocation.go | 4 +++- sign/sign.go | 2 +- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/sign.go b/sign.go index 9e393f2..47cf665 100644 --- a/sign.go +++ b/sign.go @@ -17,7 +17,7 @@ import ( ) func usage() { - log.Fatal("Usage: sign input.pdf output.pdf certificate.crt private_key.key OR verify input.pdf") + log.Fatal("Usage: sign input.pdf output.pdf certificate.crt private_key.key [chain.crt] OR verify input.pdf") } func main() { @@ -90,6 +90,33 @@ func main() { log.Fatal(err) } + certificate_chains := make([][]*x509.Certificate, 0) + + if flag.Arg(5) != "" { + certificate_pool := x509.NewCertPool() + if err != nil { + log.Fatal(err) + } + + chain_data, err := ioutil.ReadFile(flag.Arg(5)) + if err != nil { + log.Fatal(err) + } + + certificate_pool.AppendCertsFromPEM(chain_data) + certificate_chains, err = cert.Verify(x509.VerifyOptions{ + Intermediates: certificate_pool, + }) + if err != nil { + log.Fatal(err) + } + + chain_data_block, _ := pem.Decode(chain_data) + if chain_data_block == nil { + log.Fatal(errors.New("failed to parse PEM block containing the chain")) + } + } + err = sign.SignFile(input, output, sign.SignData{ Signature: sign.SignDataSignature{ Info: sign.SignDataSignatureInfo{ @@ -104,6 +131,7 @@ func main() { }, Signer: pkey, Certificate: cert, + CertificateChains: certificate_chains, TSA: sign.TSA{ URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", }, diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 00b5b8a..7066d02 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -14,6 +14,7 @@ import ( "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" + "crypto/x509" ) type pkiStatusInfo struct { @@ -28,7 +29,7 @@ type TSAResponse struct { TimeStampToken asn1.RawValue } -var signatureMaxLength = uint32(11742) +var signatureMaxLength = uint32(1000000) var signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]" func (context *SignContext) createSignaturePlaceholder() (signature string, byte_range_start_byte int64, signature_contents_start_byte int64) { @@ -139,16 +140,21 @@ func (context *SignContext) createSignature() ([]byte, error) { } if context.SignData.RevocationFunction != nil { - err = context.SignData.RevocationFunction(context.SignData.Certificate, nil, &context.SignData.RevocationData) - if err != nil { - return nil, err - } - - if context.SignData.CertificateChain != nil && len(context.SignData.CertificateChain) > 0 { - for _, cert := range context.SignData.CertificateChain { - err = context.SignData.RevocationFunction(cert, nil, &context.SignData.RevocationData) - if err != nil { - return nil, err + if (len(context.SignData.CertificateChains) > 0) { + certificate_chain := context.SignData.CertificateChains[0] + if (len(certificate_chain) > 0) { + for i, certificate := range certificate_chain { + if i < len(certificate_chain)-1 { + err = context.SignData.RevocationFunction(certificate, certificate_chain[i + 1], &context.SignData.RevocationData) + if err != nil { + return nil, err + } + } else { + err = context.SignData.RevocationFunction(certificate, nil, &context.SignData.RevocationData) + if err != nil { + return nil, err + } + } } } } @@ -160,8 +166,14 @@ func (context *SignContext) createSignature() ([]byte, error) { signer_config.ExtraSignedAttributes = append(signer_config.ExtraSignedAttributes, revocation_attribute) } + // Add the first certificate chain without our own certificate. + var certificate_chain []*x509.Certificate + if (len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1) { + certificate_chain = context.SignData.CertificateChains[0][1:] + } + // Add the signer and sign the data. - if err := signed_data.AddSignerChain(context.SignData.Certificate, context.SignData.Signer, context.SignData.CertificateChain, signer_config); err != nil { + if err := signed_data.AddSignerChain(context.SignData.Certificate, context.SignData.Signer, certificate_chain, signer_config); err != nil { return nil, err } diff --git a/sign/revocation.go b/sign/revocation.go index c969cb5..7200337 100644 --- a/sign/revocation.go +++ b/sign/revocation.go @@ -21,6 +21,7 @@ func embedOCSPRevocationStatus(cert, issuer *x509.Certificate, i *revocation.Inf ocspUrl := fmt.Sprintf("%s/%s", strings.TrimRight(cert.OCSPServer[0], "/"), base64.StdEncoding.EncodeToString(req)) + resp, err := http.Get(ocspUrl) if err != nil { return err @@ -70,7 +71,8 @@ func DefaultEmbedRevocationStatusFunction(cert, issuer *x509.Certificate, i *rev // TODO: Implement revocation status caching (required for higher volume signing) // using an OCSP server - if len(cert.OCSPServer) > 0 { + // OCSP requires issuer certificate. + if issuer != nil && len(cert.OCSPServer) > 0 { err := embedOCSPRevocationStatus(cert, issuer, i) if err != nil { return err diff --git a/sign/sign.go b/sign/sign.go index 3fe16a0..f3954f3 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -30,7 +30,7 @@ type SignData struct { Signature SignDataSignature Signer crypto.Signer Certificate *x509.Certificate - CertificateChain []*x509.Certificate + CertificateChains [][]*x509.Certificate TSA TSA RevocationData revocation.InfoArchival RevocationFunction RevocationFunction From e51daf9a8144e6f3529c201711c8bbede7c642b9 Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Thu, 13 Jul 2017 21:59:39 +0200 Subject: [PATCH 10/16] Check if signature actually fits in file --- sign/pdfsignature.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 7066d02..f37e627 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -241,6 +241,10 @@ func (context *SignContext) replaceSignature() error { dst := make([]byte, hex.EncodedLen(len(signature))) hex.Encode(dst, signature) + if uint32(len(dst)) > signatureMaxLength { + return errors.New("Signature is too big to fit in reserved space.") + } + context.OutputFile.WriteAt(dst, context.ByteRangeValues[0]+context.ByteRangeValues[1]+1) return nil From e36ea4269a7be34b7181232df6e833b55c7e304a Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Fri, 14 Jul 2017 21:08:37 +0200 Subject: [PATCH 11/16] Download TSA certificates and embed their OCSP/CRL --- sign.go | 11 +++------- sign/pdfsignature.go | 51 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/sign.go b/sign.go index 47cf665..80f5851 100644 --- a/sign.go +++ b/sign.go @@ -110,11 +110,6 @@ func main() { if err != nil { log.Fatal(err) } - - chain_data_block, _ := pem.Decode(chain_data) - if chain_data_block == nil { - log.Fatal(errors.New("failed to parse PEM block containing the chain")) - } } err = sign.SignFile(input, output, sign.SignData{ @@ -129,11 +124,11 @@ func main() { CertType: 2, Approval: false, }, - Signer: pkey, - Certificate: cert, + Signer: pkey, + Certificate: cert, CertificateChains: certificate_chains, TSA: sign.TSA{ - URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", + URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", }, RevocationData: revocation.InfoArchival{}, RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index f37e627..f710caf 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -12,9 +12,9 @@ import ( "strconv" "strings" + "crypto/x509" "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" - "crypto/x509" ) type pkiStatusInfo struct { @@ -109,6 +109,8 @@ func (context *SignContext) createSignature() ([]byte, error) { signer_config := pkcs7.SignerInfoConfig{} + TSATokenChain := make([][]*x509.Certificate, 0) + if context.SignData.TSA.URL != "" { timestamp_response, err := context.GetTSA(sign_content) if err != nil { @@ -128,6 +130,11 @@ func (context *SignContext) createSignature() ([]byte, error) { return nil, errors.New(fmt.Sprintf("%s: %s", timestamp.FailureInfo(resp.Status.FailInfo).String(), resp.Status.StatusString)) } + timestamp_p7, err := pkcs7.Parse(resp.TimeStampToken.FullBytes) + if err != nil { + return nil, err + } + if len(resp.TimeStampToken.Bytes) == 0 { return nil, errors.New("no pkcs7 data in Time-Stamp response") } @@ -137,15 +144,45 @@ func (context *SignContext) createSignature() ([]byte, error) { Value: resp.TimeStampToken, } signer_config.ExtraUnsignedAttributes = append(signer_config.ExtraUnsignedAttributes, timestamp_attribute) + + tsa_certificate_pool := x509.NewCertPool() + for _, certificate := range timestamp_p7.Certificates { + tsa_certificate_pool.AddCert(certificate) + } + + if len(timestamp_p7.Certificates) > 0 { + TSATokenChain, err = timestamp_p7.Certificates[len(timestamp_p7.Certificates)-1].Verify(x509.VerifyOptions{ + Intermediates: tsa_certificate_pool, + }) + } } if context.SignData.RevocationFunction != nil { - if (len(context.SignData.CertificateChains) > 0) { + if context.SignData.CertificateChains != nil && (len(context.SignData.CertificateChains) > 0) { certificate_chain := context.SignData.CertificateChains[0] - if (len(certificate_chain) > 0) { + if certificate_chain != nil && (len(certificate_chain) > 0) { for i, certificate := range certificate_chain { if i < len(certificate_chain)-1 { - err = context.SignData.RevocationFunction(certificate, certificate_chain[i + 1], &context.SignData.RevocationData) + err = context.SignData.RevocationFunction(certificate, certificate_chain[i+1], &context.SignData.RevocationData) + if err != nil { + return nil, err + } + } else { + err = context.SignData.RevocationFunction(certificate, nil, &context.SignData.RevocationData) + if err != nil { + return nil, err + } + } + } + } + } + + if TSATokenChain != nil && (len(TSATokenChain) > 0) { + certificate_chain := TSATokenChain[0] + if certificate_chain != nil && (len(certificate_chain) > 0) { + for i, certificate := range certificate_chain { + if i < len(certificate_chain)-1 { + err = context.SignData.RevocationFunction(certificate, certificate_chain[i+1], &context.SignData.RevocationData) if err != nil { return nil, err } @@ -168,7 +205,7 @@ func (context *SignContext) createSignature() ([]byte, error) { // Add the first certificate chain without our own certificate. var certificate_chain []*x509.Certificate - if (len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1) { + if len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1 { certificate_chain = context.SignData.CertificateChains[0][1:] } @@ -185,7 +222,9 @@ func (context *SignContext) createSignature() ([]byte, error) { func (context *SignContext) GetTSA(sign_content []byte) (timestamp_response []byte, err error) { sign_reader := bytes.NewReader(sign_content) - ts_request, err := timestamp.CreateRequest(sign_reader, nil) + ts_request, err := timestamp.CreateRequest(sign_reader, ×tamp.RequestOptions{ + Certificates: true, + }) if err != nil { return nil, err } From 8551bece65654e6445cd23774e235364f26c6d9b Mon Sep 17 00:00:00 2001 From: Paul van Brouwershaven Date: Tue, 18 Jul 2017 16:06:22 +0200 Subject: [PATCH 12/16] Signing via PKCS11 --- p11sign.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 p11sign.go diff --git a/p11sign.go b/p11sign.go new file mode 100644 index 0000000..0bca6e0 --- /dev/null +++ b/p11sign.go @@ -0,0 +1,147 @@ +package main + +import ( + "flag" + "log" + "os" + "time" + + "crypto/x509" + "io/ioutil" + + "bitbucket.org/digitorus/pdfsign/revocation" + "bitbucket.org/digitorus/pdfsign/sign" + "bitbucket.org/digitorus/pdfsign/verify" + "bitbucket.org/digitorus/pkcs11" + + p11 "github.com/miekg/pkcs11" +) + +func usage() { + log.Fatal("Usage: sign input.pdf output.pdf pkcs11-password [chain.crt] OR verify input.pdf") +} + +func main() { + flag.Parse() + + if len(flag.Args()) < 2 { + usage() + } + + method := flag.Arg(0) + if method != "sign" && method != "verify" { + usage() + } + + input := flag.Arg(1) + if len(input) == 0 { + usage() + } + + if method == "verify" { + input_file, err := os.Open(input) + if err != nil { + log.Fatal(err) + } + defer input_file.Close() + + resp, err := verify.Verify(input_file) + log.Println(resp) + if err != nil { + log.Println(err) + } + } + + if method == "sign" { + if len(flag.Args()) < 4 { + usage() + } + + output := flag.Arg(2) + if len(output) == 0 { + usage() + } + + // pkcs11 key + lib, err := pkcs11.FindLib("/lib64/libeTPkcs11.so") + if err != nil { + log.Fatal(err) + } + + // Load Library + ctx := p11.New(lib) + if ctx == nil { + log.Fatal("Failed to load library") + } + err = ctx.Initialize() + if err != nil { + log.Fatal(err) + } + // login + session, err := pkcs11.CreateSession(ctx, 0, flag.Arg(3), false) + if err != nil { + log.Fatal(err) + } + // select the first certificate + cert, ckaId, err := pkcs11.GetCert(ctx, session, nil) + if err != nil { + log.Fatal(err) + } + + // private key + pkey, err := pkcs11.InitPrivateKey(ctx, session, ckaId) + if err != nil { + log.Fatal(err) + } + + certificate_chains := make([][]*x509.Certificate, 0) + + if flag.Arg(4) != "" { + certificate_pool := x509.NewCertPool() + if err != nil { + log.Fatal(err) + } + + chain_data, err := ioutil.ReadFile(flag.Arg(5)) + if err != nil { + log.Fatal(err) + } + + certificate_pool.AppendCertsFromPEM(chain_data) + certificate_chains, err = cert.Verify(x509.VerifyOptions{ + Intermediates: certificate_pool, + }) + if err != nil { + log.Fatal(err) + } + } + + // TODO: Obtain TSA from certificate or CLI + err = sign.SignFile(input, output, sign.SignData{ + Signature: sign.SignDataSignature{ + Info: sign.SignDataSignatureInfo{ + Name: "Jeroen Bobbeldijk", + Location: "Rotterdam", + Reason: "Test", + ContactInfo: "Geen", + Date: time.Now().Local(), + }, + CertType: 2, + Approval: false, + }, + Signer: pkey, + Certificate: cert, + CertificateChains: certificate_chains, + TSA: sign.TSA{ + URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", + }, + RevocationData: revocation.InfoArchival{}, + RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, + }) + if err != nil { + log.Println(err) + } else { + log.Println("Signed PDF written to " + output) + } + } +} From b6fa7e42e09cf694497ae4e1bea8a539f9459904 Mon Sep 17 00:00:00 2001 From: Paul van Brouwershaven Date: Tue, 18 Jul 2017 16:10:29 +0200 Subject: [PATCH 13/16] Back to indirect pkcs11.New --- p11sign.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/p11sign.go b/p11sign.go index 0bca6e0..c153231 100644 --- a/p11sign.go +++ b/p11sign.go @@ -13,8 +13,6 @@ import ( "bitbucket.org/digitorus/pdfsign/sign" "bitbucket.org/digitorus/pdfsign/verify" "bitbucket.org/digitorus/pkcs11" - - p11 "github.com/miekg/pkcs11" ) func usage() { @@ -69,7 +67,7 @@ func main() { } // Load Library - ctx := p11.New(lib) + ctx := pkcs11.New(lib) if ctx == nil { log.Fatal("Failed to load library") } From 81c237871f693a04f1df98c2432eeea3492b0dbe Mon Sep 17 00:00:00 2001 From: Paul van Brouwershaven Date: Tue, 18 Jul 2017 16:16:15 +0200 Subject: [PATCH 14/16] Use correct chain_data argument --- p11sign.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p11sign.go b/p11sign.go index c153231..644f9ae 100644 --- a/p11sign.go +++ b/p11sign.go @@ -100,7 +100,7 @@ func main() { log.Fatal(err) } - chain_data, err := ioutil.ReadFile(flag.Arg(5)) + chain_data, err := ioutil.ReadFile(flag.Arg(4)) if err != nil { log.Fatal(err) } From a0393425ce710389ed538a463435d822e441516e Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Wed, 19 Jul 2017 21:33:58 +0200 Subject: [PATCH 15/16] Fix TSA --- sign.go | 2 ++ sign/pdfsignature.go | 83 ++++++++++++++++++++------------------------ verify/verify.go | 19 +++++++++- 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/sign.go b/sign.go index 80f5851..22e8931 100644 --- a/sign.go +++ b/sign.go @@ -106,6 +106,8 @@ func main() { certificate_pool.AppendCertsFromPEM(chain_data) certificate_chains, err = cert.Verify(x509.VerifyOptions{ Intermediates: certificate_pool, + CurrentTime: cert.NotBefore, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }) if err != nil { log.Fatal(err) diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index f710caf..5d94b7b 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -111,52 +111,6 @@ func (context *SignContext) createSignature() ([]byte, error) { TSATokenChain := make([][]*x509.Certificate, 0) - if context.SignData.TSA.URL != "" { - timestamp_response, err := context.GetTSA(sign_content) - if err != nil { - return nil, err - } - - var rest []byte - var resp TSAResponse - if rest, err = asn1.Unmarshal(timestamp_response, &resp); err != nil { - return nil, err - } - if len(rest) > 0 { - return nil, errors.New("trailing data in Time-Stamp response") - } - - if resp.Status.Status > 0 { - return nil, errors.New(fmt.Sprintf("%s: %s", timestamp.FailureInfo(resp.Status.FailInfo).String(), resp.Status.StatusString)) - } - - timestamp_p7, err := pkcs7.Parse(resp.TimeStampToken.FullBytes) - if err != nil { - return nil, err - } - - if len(resp.TimeStampToken.Bytes) == 0 { - return nil, errors.New("no pkcs7 data in Time-Stamp response") - } - - timestamp_attribute := pkcs7.Attribute{ - Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}, - Value: resp.TimeStampToken, - } - signer_config.ExtraUnsignedAttributes = append(signer_config.ExtraUnsignedAttributes, timestamp_attribute) - - tsa_certificate_pool := x509.NewCertPool() - for _, certificate := range timestamp_p7.Certificates { - tsa_certificate_pool.AddCert(certificate) - } - - if len(timestamp_p7.Certificates) > 0 { - TSATokenChain, err = timestamp_p7.Certificates[len(timestamp_p7.Certificates)-1].Verify(x509.VerifyOptions{ - Intermediates: tsa_certificate_pool, - }) - } - } - if context.SignData.RevocationFunction != nil { if context.SignData.CertificateChains != nil && (len(context.SignData.CertificateChains) > 0) { certificate_chain := context.SignData.CertificateChains[0] @@ -217,6 +171,43 @@ func (context *SignContext) createSignature() ([]byte, error) { // PDF needs a detached signature, meaning the content isn't included. signed_data.Detach() + if context.SignData.TSA.URL != "" { + signature_data := signed_data.GetSignedData() + + timestamp_response, err := context.GetTSA(signature_data.SignerInfos[0].EncryptedDigest) + if err != nil { + return nil, err + } + + var rest []byte + var resp TSAResponse + if rest, err = asn1.Unmarshal(timestamp_response, &resp); err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, errors.New("trailing data in Time-Stamp response") + } + + if resp.Status.Status > 0 { + return nil, errors.New(fmt.Sprintf("%s: %s", timestamp.FailureInfo(resp.Status.FailInfo).String(), resp.Status.StatusString)) + } + + _, err = pkcs7.Parse(resp.TimeStampToken.FullBytes) + if err != nil { + return nil, err + } + + if len(resp.TimeStampToken.Bytes) == 0 { + return nil, errors.New("no pkcs7 data in Time-Stamp response") + } + + timestamp_attribute := pkcs7.Attribute{ + Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}, + Value: resp.TimeStampToken, + } + signature_data.SignerInfos[0].SetUnauthenticatedAttributes([]pkcs7.Attribute{timestamp_attribute}) + } + return signed_data.Finish() } diff --git a/verify/verify.go b/verify/verify.go index e713c8e..06b7e89 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -16,6 +16,7 @@ import ( "github.com/digitorus/timestamp" "log" "golang.org/x/crypto/ocsp" + "crypto" ) type Response struct { @@ -147,6 +148,22 @@ func Verify(file *os.File) (apiResp *Response, err error) { apiResp.Error = fmt.Sprintln("Failed to parse timestamp", err) } + r := bytes.NewReader(s.EncryptedDigest) + h := crypto.SHA256.New() + b := make([]byte, 32) + for { + n, err := r.Read(b) + if err == io.EOF { + break + } + + h.Write(b[:n]) + } + + if !bytes.Equal(h.Sum(nil), signer.TimeStamp.HashedMessage) { + apiResp.Error = fmt.Sprintln("Hash in timestamp is different from pkcs7") + } + break } } @@ -217,7 +234,7 @@ func Verify(file *os.File) (apiResp *Response, err error) { signer.RevokedCertificate = true } - if len(chain) > 1 && len(chain[0]) > 1 { + if len(chain) > 0 && len(chain[0]) > 1 { issuer := chain[0][1] if resp.Certificate != nil { err = resp.Certificate.CheckSignatureFrom(issuer) From 1c9a9d60b15e833f69cd8775c8161f226a8e68cd Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Sun, 23 Jul 2017 13:10:35 +0200 Subject: [PATCH 16/16] Move fetching of revocation data, guess size of signature --- sign/pdfbyterange.go | 2 +- sign/pdfsignature.go | 93 ++++++++++++++++++++------------------------ sign/sign.go | 14 +++++++ 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/sign/pdfbyterange.go b/sign/pdfbyterange.go index 5bc3550..9a5e9f0 100644 --- a/sign/pdfbyterange.go +++ b/sign/pdfbyterange.go @@ -22,7 +22,7 @@ func (context *SignContext) updateByteRange() error { context.ByteRangeValues[1] = context.SignatureContentsStartByte - 1 // Signature ByteRange part 2 start byte directly starts after the actual signature. - context.ByteRangeValues[2] = context.ByteRangeValues[1] + 1 + int64(signatureMaxLength) + 1 + context.ByteRangeValues[2] = context.ByteRangeValues[1] + 1 + int64(context.SignatureMaxLength) + 1 // Signature ByteRange part 2 length is everything else of the file. context.ByteRangeValues[3] = output_file_size - context.ByteRangeValues[2] diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 5d94b7b..2e28f22 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -29,7 +29,6 @@ type TSAResponse struct { TimeStampToken asn1.RawValue } -var signatureMaxLength = uint32(1000000) var signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]" func (context *SignContext) createSignaturePlaceholder() (signature string, byte_range_start_byte int64, signature_contents_start_byte int64) { @@ -46,7 +45,7 @@ func (context *SignContext) createSignaturePlaceholder() (signature string, byte signature_contents_start_byte = int64(len(signature)) + 11 // Create a placeholder for the actual signature content, we wil replace it later. - signature += " /Contents<" + strings.Repeat("0", int(signatureMaxLength)) + ">" + signature += " /Contents<" + strings.Repeat("0", int(context.SignatureMaxLength)) + ">" if !context.SignData.Signature.Approval { signature += " /Reference [" // array of signature reference dictionaries @@ -88,6 +87,39 @@ func (context *SignContext) createSignaturePlaceholder() (signature string, byte return signature, byte_range_start_byte, signature_contents_start_byte } +func (context *SignContext) fetchRevocationData() error { + if context.SignData.RevocationFunction != nil { + if context.SignData.CertificateChains != nil && (len(context.SignData.CertificateChains) > 0) { + certificate_chain := context.SignData.CertificateChains[0] + if certificate_chain != nil && (len(certificate_chain) > 0) { + for i, certificate := range certificate_chain { + if i < len(certificate_chain)-1 { + err := context.SignData.RevocationFunction(certificate, certificate_chain[i+1], &context.SignData.RevocationData) + if err != nil { + return err + } + } else { + err := context.SignData.RevocationFunction(certificate, nil, &context.SignData.RevocationData) + if err != nil { + return err + } + } + } + } + } + } + + // Calculate space needed for signature. + for _, crl := range context.SignData.RevocationData.CRL { + context.SignatureMaxLength += uint32(len(crl.FullBytes) * 2) + } + for _, ocsp := range context.SignData.RevocationData.OCSP { + context.SignatureMaxLength += uint32(len(ocsp.FullBytes) * 2) + } + + return nil +} + func (context *SignContext) createSignature() ([]byte, error) { // Sadly we can't efficiently sign a file, we need to read all the bytes we want to sign. @@ -107,54 +139,13 @@ func (context *SignContext) createSignature() ([]byte, error) { return nil, err } - signer_config := pkcs7.SignerInfoConfig{} - - TSATokenChain := make([][]*x509.Certificate, 0) - - if context.SignData.RevocationFunction != nil { - if context.SignData.CertificateChains != nil && (len(context.SignData.CertificateChains) > 0) { - certificate_chain := context.SignData.CertificateChains[0] - if certificate_chain != nil && (len(certificate_chain) > 0) { - for i, certificate := range certificate_chain { - if i < len(certificate_chain)-1 { - err = context.SignData.RevocationFunction(certificate, certificate_chain[i+1], &context.SignData.RevocationData) - if err != nil { - return nil, err - } - } else { - err = context.SignData.RevocationFunction(certificate, nil, &context.SignData.RevocationData) - if err != nil { - return nil, err - } - } - } - } - } - - if TSATokenChain != nil && (len(TSATokenChain) > 0) { - certificate_chain := TSATokenChain[0] - if certificate_chain != nil && (len(certificate_chain) > 0) { - for i, certificate := range certificate_chain { - if i < len(certificate_chain)-1 { - err = context.SignData.RevocationFunction(certificate, certificate_chain[i+1], &context.SignData.RevocationData) - if err != nil { - return nil, err - } - } else { - err = context.SignData.RevocationFunction(certificate, nil, &context.SignData.RevocationData) - if err != nil { - return nil, err - } - } - } - } - } - - revocation_attribute := pkcs7.Attribute{ - Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, - Value: context.SignData.RevocationData, - } - signer_config.ExtraSignedAttributes = append(signer_config.ExtraSignedAttributes, revocation_attribute) + signer_config := pkcs7.SignerInfoConfig{ + ExtraSignedAttributes: []pkcs7.Attribute{ + { + Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, + Value: context.SignData.RevocationData, + }, + }, } // Add the first certificate chain without our own certificate. @@ -271,7 +262,7 @@ func (context *SignContext) replaceSignature() error { dst := make([]byte, hex.EncodedLen(len(signature))) hex.Encode(dst, signature) - if uint32(len(dst)) > signatureMaxLength { + if uint32(len(dst)) > context.SignatureMaxLength { return errors.New("Signature is too big to fit in reserved space.") } diff --git a/sign/sign.go b/sign/sign.go index f3954f3..0deb1de 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -73,6 +73,7 @@ type SignContext struct { ByteRangeStartByte int64 SignatureContentsStartByte int64 ByteRangeValues []int64 + SignatureMaxLength uint32 } func SignFile(input string, output string, sign_data SignData) error { @@ -143,6 +144,19 @@ func (context *SignContext) SignPDF() error { return err } + // Base size for signature. + context.SignatureMaxLength = 100000 + + // Add estimated size for TSA. + // We can't kow actual size of TSA until after signing. + if context.SignData.TSA.URL != "" { + context.SignatureMaxLength += 10000 + } + + // Fetch revocation data before adding signature placeholder. + // Revocation data can be quite large and we need to create enough space in the placeholder. + context.fetchRevocationData() + visual_signature, err := context.createVisualSignature() if err != nil { return err