diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ad625d3..03d520c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -63,7 +63,7 @@ body:
id: gpdf-version
attributes:
label: gpdf Version
- placeholder: "v0.9.2"
+ placeholder: "v1.0.0"
validations:
required: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb3b493..2f6b9e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -91,8 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- Reed-Solomon coefficient order in QR code encoder
- binary.Write return value handling for errcheck lint
-[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v0.9.0...HEAD
-[0.9.0]: https://github.com/gpdf-dev/gpdf/compare/v0.8.0...v0.9.0
+[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.0...HEAD
+[0.9.0]: https://github.com/gpdf-dev/gpdf/compare/v0.8.0...v1.0.0
[0.8.0]: https://github.com/gpdf-dev/gpdf/compare/v0.5.0...v0.8.0
[0.7.0]: https://github.com/gpdf-dev/gpdf/releases/tag/v0.7.0
[0.6.0]: https://github.com/gpdf-dev/gpdf/releases/tag/v0.6.0
diff --git a/README.md b/README.md
index ed58fae..71e702d 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,10 @@
[](https://pkg.go.dev/github.com/gpdf-dev/gpdf)
[](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
-
+
[](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[](https://go.dev/)
+[](https://gpdf.dev/)
**English** | [日本語](README_ja.md) | [中文](README_zh.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Português](README_pt.md)
diff --git a/README_es.md b/README_es.md
index c7341a4..c2e3322 100644
--- a/README_es.md
+++ b/README_es.md
@@ -4,6 +4,7 @@
[](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
[](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[](https://go.dev/)
+[](https://gpdf.dev/)
[English](README.md) | [日本語](README_ja.md) | [中文](README_zh.md) | [한국어](README_ko.md) | **Español** | [Português](README_pt.md)
diff --git a/README_ja.md b/README_ja.md
index 83b054c..48706d9 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -4,6 +4,7 @@
[](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
[](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[](https://go.dev/)
+[](https://gpdf.dev/)
[English](README.md) | **日本語** | [中文](README_zh.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Português](README_pt.md)
diff --git a/README_ko.md b/README_ko.md
index 5f2f010..bce68af 100644
--- a/README_ko.md
+++ b/README_ko.md
@@ -4,6 +4,7 @@
[](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
[](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[](https://go.dev/)
+[](https://gpdf.dev/)
[English](README.md) | [日本語](README_ja.md) | [中文](README_zh.md) | **한국어** | [Español](README_es.md) | [Português](README_pt.md)
diff --git a/README_pt.md b/README_pt.md
index 4d2c08c..dddef76 100644
--- a/README_pt.md
+++ b/README_pt.md
@@ -4,6 +4,7 @@
[](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
[](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[](https://go.dev/)
+[](https://gpdf.dev/)
[English](README.md) | [日本語](README_ja.md) | [中文](README_zh.md) | [한국어](README_ko.md) | [Español](README_es.md) | **Português**
diff --git a/README_zh.md b/README_zh.md
index a306cd6..69dfa67 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -4,6 +4,7 @@
[](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml)
[](https://goreportcard.com/report/github.com/gpdf-dev/gpdf)
[](https://go.dev/)
+[](https://gpdf.dev/)
[English](README.md) | [日本語](README_ja.md) | **中文** | [한국어](README_ko.md) | [Español](README_es.md) | [Português](README_pt.md)
diff --git a/_benchmark/go.mod b/_benchmark/go.mod
index 9048977..b3300a6 100644
--- a/_benchmark/go.mod
+++ b/_benchmark/go.mod
@@ -5,9 +5,9 @@ go 1.24
replace github.com/gpdf-dev/gpdf => ../
require (
- github.com/go-pdf/fpdf v0.9.0
+ github.com/go-pdf/fpdf v1.0.0
github.com/johnfercher/maroto/v2 v2.3.3
- github.com/gpdf-dev/gpdf v0.9.2
+ github.com/gpdf-dev/gpdf v1.0.0
github.com/signintech/gopdf v0.36.0
golang.org/x/image v0.18.0
)
diff --git a/_benchmark/go.sum b/_benchmark/go.sum
index 3dba72d..62c9eed 100644
--- a/_benchmark/go.sum
+++ b/_benchmark/go.sum
@@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg=
github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U=
-github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
-github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
+github.com/go-pdf/fpdf v1.0.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
+github.com/go-pdf/fpdf v1.0.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
diff --git a/encrypt/encrypt.go b/encrypt/encrypt.go
index 87ce351..4b7128b 100644
--- a/encrypt/encrypt.go
+++ b/encrypt/encrypt.go
@@ -94,7 +94,7 @@ func Apply(pw *pdf.Writer, opts ...Option) error {
pdf.Name("Filter"): pdf.Name("Standard"),
pdf.Name("V"): pdf.Integer(5),
pdf.Name("R"): pdf.Integer(6),
- pdf.Name("Length"): pdf.Integer(256),
+ pdf.Name("Length"): pdf.Integer(256),
pdf.Name("CF"): pdf.Dict{
pdf.Name("StdCF"): pdf.Dict{
pdf.Name("Type"): pdf.Name("CryptFilter"),
diff --git a/gpdf.go b/gpdf.go
index fb3c537..7587563 100644
--- a/gpdf.go
+++ b/gpdf.go
@@ -26,8 +26,8 @@ import (
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/encrypt"
- "github.com/gpdf-dev/gpdf/pdfa"
"github.com/gpdf-dev/gpdf/pdf"
+ "github.com/gpdf-dev/gpdf/pdfa"
"github.com/gpdf-dev/gpdf/signature"
"github.com/gpdf-dev/gpdf/template"
)
diff --git a/internal/buildinfo/version.go b/internal/buildinfo/version.go
index e9cc0f6..91ec9d3 100644
--- a/internal/buildinfo/version.go
+++ b/internal/buildinfo/version.go
@@ -3,4 +3,4 @@ package buildinfo
// Version is the library version. It is the single source of truth used by
// the public gpdf.Version constant and the default PDF Producer metadata.
-const Version = "0.9.2"
+const Version = "1.0.0"
diff --git a/pdfa/icc.go b/pdfa/icc.go
index d94948b..00e85d9 100644
--- a/pdfa/icc.go
+++ b/pdfa/icc.go
@@ -72,8 +72,8 @@ func buildSRGBProfile() []byte {
_ = binary.Write(&buf, binary.BigEndian, uint32(0)) // unicode count
// ScriptCode (empty)
_ = binary.Write(&buf, binary.BigEndian, uint16(0)) // scriptcode code
- buf.WriteByte(0) // scriptcode count
- buf.Write(make([]byte, 67)) // scriptcode string (67 bytes)
+ buf.WriteByte(0) // scriptcode count
+ buf.Write(make([]byte, 67)) // scriptcode string (67 bytes)
// Pad to 4-byte boundary
for buf.Len()%4 != 0 {
buf.WriteByte(0)
@@ -135,7 +135,7 @@ func buildSRGBProfile() []byte {
buf.Grow(totalSize)
// === Header (128 bytes) ===
- _ = binary.Write(buf, binary.BigEndian, uint32(totalSize)) // Profile size
+ _ = binary.Write(buf, binary.BigEndian, uint32(totalSize)) // Profile size
buf.WriteString("gpdf") // Preferred CMM Type
_ = binary.Write(buf, binary.BigEndian, uint32(0x02100000)) // Version 2.1.0
buf.WriteString("mntr") // Device class: monitor
@@ -148,13 +148,13 @@ func buildSRGBProfile() []byte {
_ = binary.Write(buf, binary.BigEndian, uint16(0)) // hour
_ = binary.Write(buf, binary.BigEndian, uint16(0)) // minute
_ = binary.Write(buf, binary.BigEndian, uint16(0)) // second
- buf.WriteString("acsp") // File signature (always "acsp")
- buf.WriteString("APPL") // Primary platform
- _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Profile flags
- buf.WriteString("gpdf") // Device manufacturer
- buf.WriteString("sRGB") // Device model
- _ = binary.Write(buf, binary.BigEndian, uint64(0)) // Device attributes
- _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Rendering intent (perceptual)
+ buf.WriteString("acsp") // File signature (always "acsp")
+ buf.WriteString("APPL") // Primary platform
+ _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Profile flags
+ buf.WriteString("gpdf") // Device manufacturer
+ buf.WriteString("sRGB") // Device model
+ _ = binary.Write(buf, binary.BigEndian, uint64(0)) // Device attributes
+ _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Rendering intent (perceptual)
// PCS illuminant (D50 XYZ)
_ = binary.Write(buf, binary.BigEndian, s15Fixed16(0.9642))
_ = binary.Write(buf, binary.BigEndian, s15Fixed16(1.0))
diff --git a/pdfa/verify.go b/pdfa/verify.go
index 676f66a..e78a806 100644
--- a/pdfa/verify.go
+++ b/pdfa/verify.go
@@ -86,38 +86,49 @@ func ParseConformance(pdfData []byte) *Conformance {
// Validate checks all PDF/A conformance requirements and returns a list of violations.
func (c *Conformance) Validate() []string {
var violations []string
+ violations = c.validateXMP(violations)
+ violations = c.validateOutputIntents(violations)
+ violations = c.validateICC(violations)
- // XMP checks
+ for _, elem := range c.ForbiddenElements {
+ violations = append(violations, fmt.Sprintf("forbidden element found: %s", elem))
+ }
+
+ return violations
+}
+
+func (c *Conformance) validateXMP(violations []string) []string {
if c.XMP == nil {
- violations = append(violations, "XMP metadata not found")
- } else {
- if !c.XMP.HasXPacket {
- violations = append(violations, "XMP missing xpacket header/footer")
- }
- if c.Part == 0 {
- violations = append(violations, "pdfaid:part not found or invalid")
- }
- if c.Level == "" {
- violations = append(violations, "pdfaid:conformance not found")
- }
- if c.XMP.Title == "" {
- violations = append(violations, "XMP dc:title is empty (recommended)")
- }
- if c.XMP.Producer == "" {
- violations = append(violations, "XMP pdf:Producer is empty")
- }
- if c.XMP.CreatorTool == "" {
- violations = append(violations, "XMP xmp:CreatorTool is empty")
- }
- if c.XMP.CreateDate == "" {
- violations = append(violations, "XMP xmp:CreateDate is empty")
- }
- if c.XMP.ModifyDate == "" {
- violations = append(violations, "XMP xmp:ModifyDate is empty")
- }
+ return append(violations, "XMP metadata not found")
+ }
+ if !c.XMP.HasXPacket {
+ violations = append(violations, "XMP missing xpacket header/footer")
+ }
+ if c.Part == 0 {
+ violations = append(violations, "pdfaid:part not found or invalid")
+ }
+ if c.Level == "" {
+ violations = append(violations, "pdfaid:conformance not found")
+ }
+ if c.XMP.Title == "" {
+ violations = append(violations, "XMP dc:title is empty (recommended)")
}
+ if c.XMP.Producer == "" {
+ violations = append(violations, "XMP pdf:Producer is empty")
+ }
+ if c.XMP.CreatorTool == "" {
+ violations = append(violations, "XMP xmp:CreatorTool is empty")
+ }
+ if c.XMP.CreateDate == "" {
+ violations = append(violations, "XMP xmp:CreateDate is empty")
+ }
+ if c.XMP.ModifyDate == "" {
+ violations = append(violations, "XMP xmp:ModifyDate is empty")
+ }
+ return violations
+}
- // OutputIntent checks
+func (c *Conformance) validateOutputIntents(violations []string) []string {
if !c.HasOutputIntents {
violations = append(violations, "/OutputIntents missing from catalog")
}
@@ -133,33 +144,28 @@ func (c *Conformance) Validate() []string {
if !c.HasMetadataStream {
violations = append(violations, "Metadata stream (/Subtype /XML) missing")
}
+ return violations
+}
- // ICC profile checks
+func (c *Conformance) validateICC(violations []string) []string {
if c.ICCInfo == nil {
- violations = append(violations, "ICC color profile not found")
- } else {
- if !c.ICCInfo.HasACSP {
- violations = append(violations, "ICC profile missing 'acsp' signature")
- }
- if c.ICCInfo.ColorSpace != "RGB " {
- violations = append(violations, fmt.Sprintf("ICC color space = %q, want 'RGB '", c.ICCInfo.ColorSpace))
- }
- if c.ICCInfo.DeviceClass != "mntr" {
- violations = append(violations, fmt.Sprintf("ICC device class = %q, want 'mntr'", c.ICCInfo.DeviceClass))
- }
- if c.Part == 1 && !strings.HasPrefix(c.ICCInfo.Version, "2.") {
- violations = append(violations, fmt.Sprintf("PDF/A-1b requires ICC v2.x, got %s", c.ICCInfo.Version))
- }
- for _, tag := range c.ICCInfo.MissingTags {
- violations = append(violations, fmt.Sprintf("ICC profile missing required tag: %s", tag))
- }
+ return append(violations, "ICC color profile not found")
}
-
- // Forbidden elements
- for _, elem := range c.ForbiddenElements {
- violations = append(violations, fmt.Sprintf("forbidden element found: %s", elem))
+ if !c.ICCInfo.HasACSP {
+ violations = append(violations, "ICC profile missing 'acsp' signature")
+ }
+ if c.ICCInfo.ColorSpace != "RGB " {
+ violations = append(violations, fmt.Sprintf("ICC color space = %q, want 'RGB '", c.ICCInfo.ColorSpace))
+ }
+ if c.ICCInfo.DeviceClass != "mntr" {
+ violations = append(violations, fmt.Sprintf("ICC device class = %q, want 'mntr'", c.ICCInfo.DeviceClass))
+ }
+ if c.Part == 1 && !strings.HasPrefix(c.ICCInfo.Version, "2.") {
+ violations = append(violations, fmt.Sprintf("PDF/A-1b requires ICC v2.x, got %s", c.ICCInfo.Version))
+ }
+ for _, tag := range c.ICCInfo.MissingTags {
+ violations = append(violations, fmt.Sprintf("ICC profile missing required tag: %s", tag))
}
-
return violations
}
@@ -212,7 +218,21 @@ func parseXMP(pdfStr string) (*XMPInfo, error) {
// Extract XMP between xpacket markers
xmpRegion := pdfStr[beginIdx : endIdx+len("")]
- // Find ...
+ rdf, err := decodeRDF(xmpRegion)
+ if err != nil {
+ return nil, err
+ }
+
+ // Extract values from all Description elements
+ for _, desc := range rdf.Descriptions {
+ mergeDescription(info, &desc)
+ }
+
+ return info, nil
+}
+
+// decodeRDF extracts and decodes the rdf:RDF element from an XMP region.
+func decodeRDF(xmpRegion string) (*xmpRDF, error) {
rdfStart := strings.Index(xmpRegion, "")
if rdfStart < 0 || rdfEnd < 0 {
@@ -220,45 +240,59 @@ func parseXMP(pdfStr string) (*XMPInfo, error) {
}
rdfXML := xmpRegion[rdfStart : rdfEnd+len("")]
- // Parse with xml.Decoder
var rdf xmpRDF
decoder := xml.NewDecoder(strings.NewReader(rdfXML))
if err := decoder.Decode(&rdf); err != nil {
return nil, fmt.Errorf("XML decode: %w", err)
}
+ return &rdf, nil
+}
- // Extract values from all Description elements
- for _, desc := range rdf.Descriptions {
- if desc.Title != nil && len(desc.Title.Items) > 0 && desc.Title.Items[0].Value != "" {
- info.Title = desc.Title.Items[0].Value
- }
- if desc.Creator != nil && len(desc.Creator.Items) > 0 && desc.Creator.Items[0].Value != "" {
- info.Creator = desc.Creator.Items[0].Value
- }
- if desc.Description != nil && len(desc.Description.Items) > 0 && desc.Description.Items[0].Value != "" {
- info.Subject = desc.Description.Items[0].Value
- }
- if desc.CreatorTool != "" {
- info.CreatorTool = desc.CreatorTool
- }
- if desc.CreateDate != "" {
- info.CreateDate = desc.CreateDate
- }
- if desc.ModifyDate != "" {
- info.ModifyDate = desc.ModifyDate
- }
- if desc.Producer != "" {
- info.Producer = desc.Producer
- }
- if desc.Part > 0 {
- info.PDFAPart = desc.Part
- }
- if desc.Conformance != "" {
- info.PDFAConf = desc.Conformance
- }
+// altValue returns the first non-empty value from an xmpAlt, or "".
+func altValue(a *xmpAlt) string {
+ if a != nil && len(a.Items) > 0 {
+ return a.Items[0].Value
}
+ return ""
+}
- return info, nil
+// seqValue returns the first non-empty value from an xmpSeq, or "".
+func seqValue(s *xmpSeq) string {
+ if s != nil && len(s.Items) > 0 {
+ return s.Items[0].Value
+ }
+ return ""
+}
+
+// mergeDescription merges fields from an xmpDescription into XMPInfo.
+func mergeDescription(info *XMPInfo, desc *xmpDescription) {
+ if v := altValue(desc.Title); v != "" {
+ info.Title = v
+ }
+ if v := seqValue(desc.Creator); v != "" {
+ info.Creator = v
+ }
+ if v := altValue(desc.Description); v != "" {
+ info.Subject = v
+ }
+ if desc.CreatorTool != "" {
+ info.CreatorTool = desc.CreatorTool
+ }
+ if desc.CreateDate != "" {
+ info.CreateDate = desc.CreateDate
+ }
+ if desc.ModifyDate != "" {
+ info.ModifyDate = desc.ModifyDate
+ }
+ if desc.Producer != "" {
+ info.Producer = desc.Producer
+ }
+ if desc.Part > 0 {
+ info.PDFAPart = desc.Part
+ }
+ if desc.Conformance != "" {
+ info.PDFAConf = desc.Conformance
+ }
}
// --- ICC profile parsing ---
diff --git a/signature/byterange.go b/signature/byterange.go
index 0495954..0c83ded 100644
--- a/signature/byterange.go
+++ b/signature/byterange.go
@@ -112,8 +112,8 @@ func buildSignedPDF(pdfData []byte, signer Signer, cfg *signConfig) (*signResult
br := [4]int64{
0,
- int64(sigStart) - 1, // up to and excluding '<'
- int64(sigEnd) + 1, // after '>'
+ int64(sigStart) - 1, // up to and excluding '<'
+ int64(sigEnd) + 1, // after '>'
int64(len(result)) - int64(sigEnd) - 1,
}
diff --git a/signature/cms.go b/signature/cms.go
index 6a6e419..9868979 100644
--- a/signature/cms.go
+++ b/signature/cms.go
@@ -63,68 +63,34 @@ type attribute struct {
Values asn1.RawValue `asn1:"set"`
}
-// createCMSSignature creates a CMS/PKCS#7 SignedData structure.
-func createCMSSignature(hash []byte, signer Signer, cfg *signConfig) ([]byte, error) {
- cert := signer.Certificate
-
- // Determine signature algorithm
- var sigAlg pkix.AlgorithmIdentifier
- switch signer.PrivateKey.(type) {
+// signatureAlgorithm returns the AlgorithmIdentifier for the given private key.
+func signatureAlgorithm(key crypto.PrivateKey) (pkix.AlgorithmIdentifier, error) {
+ switch key.(type) {
case *rsa.PrivateKey:
- sigAlg = pkix.AlgorithmIdentifier{Algorithm: oidRSAWithSHA256}
+ return pkix.AlgorithmIdentifier{Algorithm: oidRSAWithSHA256}, nil
case *ecdsa.PrivateKey:
- sigAlg = pkix.AlgorithmIdentifier{Algorithm: oidECDSAWithSHA256}
+ return pkix.AlgorithmIdentifier{Algorithm: oidECDSAWithSHA256}, nil
default:
- return nil, fmt.Errorf("unsupported key type: %T", signer.PrivateKey)
- }
-
- digestAlg := pkix.AlgorithmIdentifier{Algorithm: oidSHA256}
-
- // Build signed attributes
- signTime := cfg.signTime
- if signTime.IsZero() {
- signTime = time.Now()
- }
-
- attrs, err := buildSignedAttrs(hash, signTime)
- if err != nil {
- return nil, fmt.Errorf("build signed attrs: %w", err)
- }
-
- // Marshal signed attributes as a SEQUENCE (the default for []attribute)
- attrsBytes, err := marshalAttributes(attrs)
- if err != nil {
- return nil, fmt.Errorf("marshal attrs: %w", err)
+ return pkix.AlgorithmIdentifier{}, fmt.Errorf("unsupported key type: %T", key)
}
+}
- // For signing, the authenticated attributes must be encoded as a SET
- // (tag 0x31) rather than the IMPLICIT [0] (tag 0xA0) used in SignerInfo.
- attrsBytesForSign := make([]byte, len(attrsBytes))
- copy(attrsBytesForSign, attrsBytes)
- attrsBytesForSign[0] = 0x31 // SET tag
-
- attrHash := crypto.SHA256.New()
- attrHash.Write(attrsBytesForSign)
- attrDigest := attrHash.Sum(nil)
-
- // Create signature
- var sig []byte
- switch key := signer.PrivateKey.(type) {
+// computeSignature signs the digest with the given private key.
+func computeSignature(key crypto.PrivateKey, digest []byte) ([]byte, error) {
+ switch k := key.(type) {
case *rsa.PrivateKey:
- sig, err = rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, attrDigest)
+ return rsa.SignPKCS1v15(rand.Reader, k, crypto.SHA256, digest)
case *ecdsa.PrivateKey:
- sig, err = ecdsa.SignASN1(rand.Reader, key, attrDigest)
- }
- if err != nil {
- return nil, fmt.Errorf("sign: %w", err)
+ return ecdsa.SignASN1(rand.Reader, k, digest)
+ default:
+ return nil, fmt.Errorf("unsupported key type: %T", key)
}
+}
- // Build SignerInfo
+// buildSignerInfoBytes builds and marshals the SignerInfo structure.
+func buildSignerInfoBytes(cert *x509.Certificate, attrsBytes []byte, sig []byte, digestAlg, sigAlg pkix.AlgorithmIdentifier) ([]byte, error) {
issuerRaw := asn1.RawValue{FullBytes: cert.RawIssuer}
- // Build SignedAttrs as IMPLICIT [0] tagged value
- // attrsBytes is a SEQUENCE containing the attribute elements.
- // We need to extract just the inner content bytes.
innerAttrsBytes, err := extractInnerBytes(attrsBytes)
if err != nil {
return nil, fmt.Errorf("extract inner attrs: %w", err)
@@ -153,8 +119,50 @@ func createCMSSignature(hash []byte, signer Signer, cfg *signConfig) ([]byte, er
Signature: sig,
}
- // Marshal signer info
- siBytes, err := asn1.Marshal(si)
+ return asn1.Marshal(si)
+}
+
+// createCMSSignature creates a CMS/PKCS#7 SignedData structure.
+func createCMSSignature(hash []byte, signer Signer, cfg *signConfig) ([]byte, error) {
+ cert := signer.Certificate
+
+ sigAlg, err := signatureAlgorithm(signer.PrivateKey)
+ if err != nil {
+ return nil, err
+ }
+
+ digestAlg := pkix.AlgorithmIdentifier{Algorithm: oidSHA256}
+
+ signTime := cfg.signTime
+ if signTime.IsZero() {
+ signTime = time.Now()
+ }
+
+ attrs, err := buildSignedAttrs(hash, signTime)
+ if err != nil {
+ return nil, fmt.Errorf("build signed attrs: %w", err)
+ }
+
+ attrsBytes, err := marshalAttributes(attrs)
+ if err != nil {
+ return nil, fmt.Errorf("marshal attrs: %w", err)
+ }
+
+ // For signing, authenticated attributes must be encoded as SET (0x31)
+ attrsBytesForSign := make([]byte, len(attrsBytes))
+ copy(attrsBytesForSign, attrsBytes)
+ attrsBytesForSign[0] = 0x31
+
+ attrHash := crypto.SHA256.New()
+ attrHash.Write(attrsBytesForSign)
+ attrDigest := attrHash.Sum(nil)
+
+ sig, err := computeSignature(signer.PrivateKey, attrDigest)
+ if err != nil {
+ return nil, fmt.Errorf("sign: %w", err)
+ }
+
+ siBytes, err := buildSignerInfoBytes(cert, attrsBytes, sig, digestAlg, sigAlg)
if err != nil {
return nil, fmt.Errorf("marshal signer info: %w", err)
}
diff --git a/signature/verify.go b/signature/verify.go
index 9094f0d..00a6930 100644
--- a/signature/verify.go
+++ b/signature/verify.go
@@ -314,7 +314,7 @@ func extractContentsHex(s string) ([]byte, error) {
re := regexp.MustCompile(`/Contents\s*<([0-9A-Fa-f]+)>`)
m := re.FindStringSubmatch(s)
if m == nil {
- return nil, fmt.Errorf("Contents hex string not found")
+ return nil, fmt.Errorf("contents hex string not found")
}
hexStr := strings.TrimRight(m[1], "0") // remove trailing zero padding
if len(hexStr)%2 != 0 {