Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions sign/pdfsignature.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,14 +325,23 @@ func (context *SignContext) createSignature() ([]byte, error) {
return nil, fmt.Errorf("new signed data: %w", err)
}

signer_config := pkcs7.SignerInfoConfig{
ExtraSignedAttributes: []pkcs7.Attribute{
{
Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8},
Value: context.SignData.RevocationData,
},
*signingCertificate,
extraAttrs := []pkcs7.Attribute{
{
Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8},
Value: context.SignData.RevocationData,
},
*signingCertificate,
}
// Append caller-supplied custom signed attributes after the
// library defaults. They ride inside the cryptographically
// protected SignedAttributes set per RFC 5652 §11.2; any
// tampering with their values breaks pkcs7.Verify. An empty
// slice preserves prior behavior exactly.
if len(context.SignData.ExtraSignedAttributes) > 0 {
extraAttrs = append(extraAttrs, context.SignData.ExtraSignedAttributes...)
}
signer_config := pkcs7.SignerInfoConfig{
ExtraSignedAttributes: extraAttrs,
}

// Add the first certificate chain without our own certificate.
Expand Down
88 changes: 88 additions & 0 deletions sign/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"io"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/digitorus/pdf"
"github.com/digitorus/pdfsign/revocation"
"github.com/digitorus/pdfsign/verify"
"github.com/digitorus/pkcs7"
"github.com/mattetti/filebuffer"
)

Expand Down Expand Up @@ -281,6 +283,92 @@ func TestSignPDFFileUTF8(t *testing.T) {
}
}

// TestSignPDF_ExtraSignedAttributes_AppearInPKCS7 — caller-supplied
// custom signed attributes must ride inside the cryptographically
// protected PKCS#7 SignedAttributes set so a downstream
// pkcs7.UnmarshalSignedAttribute can recover them by OID.
func TestSignPDF_ExtraSignedAttributes_AppearInPKCS7(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)

// A test-only OID under the IANA "private experimental" arc.
customOID := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1, 1}
customValue := []byte("test content hash")

tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
defer func() { _ = os.Remove(tmpfile.Name()) }()

err = SignFile("../testfiles/testfile20.pdf", tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "Extra Attrs Tester",
Reason: "Test ExtraSignedAttributes",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: CertificationSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
DigestAlgorithm: crypto.SHA256,
Signer: pkey,
Certificate: cert,
ExtraSignedAttributes: []pkcs7.Attribute{
{Type: customOID, Value: customValue},
},
})
if err != nil {
t.Fatalf("SignFile: %s", err.Error())
}

// Re-open the signed PDF, walk to the signature dictionary, parse
// the PKCS#7 contents, and recover the custom attribute by OID.
data, err := os.ReadFile(tmpfile.Name())
if err != nil {
t.Fatalf("read signed file: %s", err.Error())
}
rdr, err := pdf.NewReader(filebuffer.New(data), int64(len(data)))
if err != nil {
t.Fatalf("pdf.NewReader: %s", err.Error())
}

var sigContents string
for _, ent := range rdr.Trailer().Key("Root").Key("AcroForm").Key("Fields").Keys() {
_ = ent
}
// Walk the AcroForm field tree (pdfsign places the signature value
// in the first field's /V dict).
fields := rdr.Trailer().Key("Root").Key("AcroForm").Key("Fields")
for i := 0; i < fields.Len(); i++ {
v := fields.Index(i).Key("V")
if v.IsNull() {
continue
}
raw := v.Key("Contents").RawString()
if raw != "" {
sigContents = raw
break
}
}
if sigContents == "" {
t.Fatal("could not locate /Contents in any signature dict")
}

p7, err := pkcs7.Parse([]byte(sigContents))
if err != nil {
t.Fatalf("pkcs7.Parse: %s", err.Error())
}

var got []byte
if err := p7.UnmarshalSignedAttribute(customOID, &got); err != nil {
t.Fatalf("UnmarshalSignedAttribute: %s", err.Error())
}
if string(got) != string(customValue) {
t.Fatalf("attribute value mismatch: want %q, got %q", customValue, got)
}
}

func BenchmarkSignPDF(b *testing.B) {
cert, pkey := loadCertificateAndKey(&testing.T{})
certificateChains := [][]*x509.Certificate{}
Expand Down
18 changes: 18 additions & 0 deletions sign/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/digitorus/pdf"
"github.com/digitorus/pdfsign/revocation"
"github.com/digitorus/pkcs7"
"github.com/mattetti/filebuffer"
)

Expand Down Expand Up @@ -35,6 +36,23 @@ type SignData struct {
RevocationFunction RevocationFunction
Appearance Appearance

// ExtraSignedAttributes lets callers append additional CMS
// SignedAttributes (RFC 5652 §11) keyed by custom OIDs to the
// PKCS#7 signature, in addition to the library defaults
// (Adobe RevocationData OID 1.2.840.113583.1.1.8 and the
// signing-certificate-v2 attribute).
//
// These attributes ride inside the cryptographically protected
// SignedAttributes set, so any tampering with their values
// breaks pkcs7.Verify. Use cases include embedding a canonical
// content hash for downstream tamper detection (ICP-Brasil
// DOC-ICP-15.03 §6.4-style integrity attestations) or
// transport-specific provenance OIDs.
//
// An empty slice is the default and preserves the prior
// behavior exactly.
ExtraSignedAttributes []pkcs7.Attribute

objectId uint32
}

Expand Down