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/p11sign.go b/p11sign.go new file mode 100644 index 0000000..644f9ae --- /dev/null +++ b/p11sign.go @@ -0,0 +1,145 @@ +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" +) + +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 := pkcs11.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(4)) + 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) + } + } +} diff --git a/revocation/revocation.go b/revocation/revocation.go new file mode 100644 index 0000000..0c44148 --- /dev/null +++ b/revocation/revocation.go @@ -0,0 +1,55 @@ +package revocation + +import ( + "crypto/x509" + "encoding/asn1" +) + +// 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.go b/sign.go new file mode 100644 index 0000000..22e8931 --- /dev/null +++ b/sign.go @@ -0,0 +1,144 @@ +package main + +import ( + "flag" + "log" + "os" + "time" + + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + + "bitbucket.org/digitorus/pdfsign/revocation" + "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 [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()) < 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.ParsePKCS1PrivateKey(key_data_block.Bytes) + if err != nil { + 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, + CurrentTime: cert.NotBefore, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + if err != nil { + log.Fatal(err) + } + } + + 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) + } + } +} diff --git a/sign/helpers.go b/sign/helpers.go new file mode 100644 index 0000000..91472a2 --- /dev/null +++ b/sign/helpers.go @@ -0,0 +1,125 @@ +package sign + +import ( + "bitbucket.org/digitorus/pdf" + "errors" + "fmt" + "io" + "math" + "os" + "strings" + "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) + 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..9a5e9f0 --- /dev/null +++ b/sign/pdfbyterange.go @@ -0,0 +1,44 @@ +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() + + output_file_size := output_file_stat.Size() + + // 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 - 1 + + // Signature ByteRange part 2 start byte directly starts after the actual signature. + 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] + + 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..b8957b8 --- /dev/null +++ b/sign/pdfcatalog.go @@ -0,0 +1,59 @@ +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.VisualSignData.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 += " >>" + + 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/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 new file mode 100644 index 0000000..2e28f22 --- /dev/null +++ b/sign/pdfsignature.go @@ -0,0 +1,272 @@ +package sign + +import ( + "bytes" + "encoding/asn1" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "strconv" + "strings" + + "crypto/x509" + "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 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(context.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) 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. + context.OutputFile.Seek(0, 0) + sign_buf := bytes.NewBuffer(nil) + io.Copy(sign_buf, context.OutputFile) + file_content := sign_buf.Bytes() + + // 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])]...) + 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 + } + + 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. + 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, certificate_chain, signer_config); err != nil { + return nil, err + } + + // 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() +} + +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, ×tamp.RequestOptions{ + Certificates: true, + }) + 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 { + return err + } + + dst := make([]byte, hex.EncodedLen(len(signature))) + hex.Encode(dst, signature) + + if uint32(len(dst)) > context.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 +} diff --git a/sign/pdftrailer.go b/sign/pdftrailer.go new file mode 100644 index 0000000..e73b792 --- /dev/null +++ b/sign/pdftrailer.go @@ -0,0 +1,51 @@ +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+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 { + 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")); err != nil { + return err + } + + return nil +} diff --git a/sign/pdfvisualsignature.go b/sign/pdfvisualsignature.go new file mode 100644 index 0000000..bbc9007 --- /dev/null +++ b/sign/pdfvisualsignature.go @@ -0,0 +1,50 @@ +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" + + first_page, err := findFirstPage(root.Key("Pages")) + if err != nil { + return "", err + } + + 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" + 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 new file mode 100644 index 0000000..566e6cb --- /dev/null +++ b/sign/pdfxref.go @@ -0,0 +1,73 @@ +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+4, 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. + 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. + if _, err := context.OutputFile.Write([]byte(catalog_xref_line)); err != nil { + return err + } + + // Create the new signature xref line. + 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 { + return err + } + + return nil +} diff --git a/sign/revocation.go b/sign/revocation.go new file mode 100644 index 0000000..7200337 --- /dev/null +++ b/sign/revocation.go @@ -0,0 +1,91 @@ +package sign + +import ( + "crypto/x509" + "encoding/base64" + "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 + } + + return i.AddOCSP(body) +} + +// 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 + return i.AddCRL(body) +} + +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 + // 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 + // OCSP requires issuer certificate. + if issuer != nil && len(cert.OCSPServer) > 0 { + err := embedOCSPRevocationStatus(cert, issuer, i) + if err != nil { + return err + } + } + + // using a crl + if len(cert.CRLDistributionPoints) > 0 { + err := embedCRLRevocationStatus(cert, issuer, i) + if err != nil { + return err + } + } + + return nil +} diff --git a/sign/revocation_test.go b/sign/revocation_test.go new file mode 100644 index 0000000..7542aac --- /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 := DefaultEmbedRevocationStatusFunction(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/sign/sign.go b/sign/sign.go new file mode 100644 index 0000000..0deb1de --- /dev/null +++ b/sign/sign.go @@ -0,0 +1,238 @@ +package sign + +import ( + "crypto" + "crypto/x509" + "io" + "os" + "time" + + "bitbucket.org/digitorus/pdf" + "bitbucket.org/digitorus/pdfsign/revocation" +) + +type CatalogData struct { + ObjectId uint32 + Length int64 + RootString string +} + +type TSA struct { + URL string + Username string + Password string +} + +type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error + +type SignData struct { + ObjectId uint32 + Signature SignDataSignature + Signer crypto.Signer + Certificate *x509.Certificate + CertificateChains [][]*x509.Certificate + TSA TSA + RevocationData revocation.InfoArchival + RevocationFunction RevocationFunction +} + +type VisualSignData struct { + ObjectId uint32 + Length int64 +} + +type InfoData struct { + ObjectId uint32 + Length int64 +} + +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 { + Filesize int64 + InputFile *os.File + OutputFile *os.File + SignData SignData + CatalogData CatalogData + VisualSignData VisualSignData + InfoData InfoData + PDFReader *pdf.Reader + NewXrefStart int64 + ByteRangeStartByte int64 + SignatureContentsStartByte int64 + ByteRangeValues []int64 + SignatureMaxLength uint32 +} + +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) + 3 + + // We do size+1 because we insert a newline. + context := SignContext{ + Filesize: size + 1, + PDFReader: rdr, + InputFile: input_file, + OutputFile: output_file, + VisualSignData: VisualSignData{ + ObjectId: uint32(rdr.XrefInformation.ItemCount), + }, + CatalogData: CatalogData{ + ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1, + }, + InfoData: InfoData{ + ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2, + }, + SignData: sign_data, + } + + err = context.SignPDF() + if err != nil { + return err + } + + return nil +} + +func (context *SignContext) SignPDF() error { + // 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 + } + + // 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 + } + + context.VisualSignData.Length = int64(len(visual_signature)) + + // Write the new catalog object. + if _, err := context.OutputFile.Write([]byte(visual_signature)); 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() + + 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 += appended_bytes + signature_contents_start_byte += appended_bytes + + 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 = appended_bytes + int64(len(signature_object)) + + 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 + } + + err = context.OutputFile.Sync() + if err != nil { + return err + } + + return nil +} diff --git a/signatures.go b/verify/verify.go similarity index 86% rename from signatures.go rename to verify/verify.go index e3cad70..06b7e89 100644 --- a/signatures.go +++ b/verify/verify.go @@ -1,306 +1,313 @@ -// 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" + "bitbucket.org/digitorus/pdfsign/revocation" + "github.com/digitorus/pkcs7" + "github.com/digitorus/timestamp" + "log" + "golang.org/x/crypto/ocsp" + "crypto" +) + +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() + + file.Seek(0, 0) + + rdr, err := pdf.NewReader(file, size) + if err != nil { + return nil, fmt.Errorf("Failed to open file: %v", err) + } + + // 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) + } + + 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 + } + } + } + + // 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 + } + log.Println("Invalid sig") + apiResp.Error = fmt.Sprintln("Failed to verify signature:", err) + } else { + log.Println("Valid sig") + signer.ValidSignature = true + signer.TrustedIssuer = true + } + + // PDF signature certificate revocation information attribute (1.2.840.113583.1.1.8) + var revInfo revocation.InfoArchival + 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) > 0 && 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) + } + } +}