Signature System Architecture
The signature system handles certificate discovery, PDF signing, and verification.
Overview
┌─────────────────────────────────────────────────────────────┐
│ SignatureService │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Certificate Aggregation │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ PKCS#12 │ │ PKCS#11 │ │ NSS │ │ │
│ │ │ Loader │ │ Loader │ │ Loader │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │ │
│ │ └───────────┼───────────┘ │ │
│ │ ▼ │ │
│ │ []types.Certificate │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Signing │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ PKCS#12 │ │ PKCS#11 │ │ NSS │ │ │
│ │ │ Signer │ │ Signer │ │ Signer │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │ │
│ │ └───────────┼───────────┘ │ │
│ │ ▼ │ │
│ │ digitorus/pdfsign │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Profile Management │ │
│ │ SignatureProfile → Appearance │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Certificate Types
Certificate Structure
// internal/signature/types/types.go
type Certificate struct {
Name string `json:"name"`
Issuer string `json:"issuer"`
Subject string `json:"subject"`
SerialNumber string `json:"serialNumber"`
ValidFrom string `json:"validFrom"`
ValidTo string `json:"validTo"`
Fingerprint string `json:"fingerprint"` // SHA-256, unique ID
Source string `json:"source"` // pkcs12, pkcs11, nss
KeyUsage []string `json:"keyUsage"`
IsValid bool `json:"isValid"` // Within validity period
CanSign bool `json:"canSign"` // Has signing capability
RequiresPin bool `json:"requiresPin"`
FilePath string `json:"filePath,omitempty"`
PKCS11Module string `json:"pkcs11Module,omitempty"`
}
Fingerprint Generation
// internal/signature/certutil/convert.go
hash := sha256.Sum256(cert.Raw)
fingerprint := hex.EncodeToString(hash[:])
The SHA-256 fingerprint uniquely identifies certificates across all sources.
Certificate Sources
PKCS#12 (.p12, .pfx files)
Self-contained certificate + private key files:
// internal/signature/pkcs12/pkcs12.go
func LoadCertificatesFromPath(dir string) ([]types.Certificate, error) {
// Walk directory for .p12/.pfx files
// Parse each file (without password - just read cert info)
// Return certificate metadata
}
func LoadSignerFromFile(path, password string) (*Signer, error) {
// Read file
// Decrypt with password
// Return signer with private key access
}
PKCS#11 (Hardware Tokens)
Smart cards, USB tokens, HSMs via PKCS#11 API:
// internal/signature/pkcs11/loader.go
func LoadCertificatesFromModules(modulePaths []string) ([]types.Certificate, error) {
for _, modulePath := range modulePaths {
// Validate module (security check)
validatePKCS11Module(modulePath)
// Load module
p := pkcs11.New(modulePath)
p.Initialize()
// Enumerate slots and certificates
slots, _ := p.GetSlotList(true)
for _, slot := range slots {
// Find certificate objects
// Extract certificate data
}
}
}
Module Validation:
func validatePKCS11Module(modulePath string) error {
// Check file exists and is regular file
// Verify reasonable file size (1KB - 200MB)
// Check file is readable
}
NSS (Browser Databases)
Firefox/Chrome certificate stores via CGO:
// internal/signature/nss/nss.go
/*
#cgo pkg-config: nss
#include <nss.h>
#include <pk11pub.h>
#include <cert.h>
*/
import "C"
func ListCertificates() ([]Certificate, error) {
// Initialize NSS with user's database
C.nss_init(nssDBPath)
// Get all certificates
certList := C.get_all_certs()
// Filter for those with private keys
// Convert to Go structures
}
Signer Interface
All signing backends implement crypto.Signer:
// internal/signature/signer.go
type CertificateSigner interface {
crypto.Signer
Certificate() *x509.Certificate
}
This allows pdfsign to use any backend uniformly.
PKCS#12 Signer
type Signer struct {
cert *x509.Certificate
privateKey crypto.PrivateKey
}
func (ps *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
if signer, ok := ps.privateKey.(crypto.Signer); ok {
return signer.Sign(rand, digest, opts)
}
return nil, fmt.Errorf("private key does not implement crypto.Signer")
}
PKCS#11 Signer
type Signer struct {
cert *x509.Certificate
keyHandle pkcs11.ObjectHandle
session pkcs11.SessionHandle
p *pkcs11.Ctx
}
func (ps *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
// Prepare DigestInfo for RSA PKCS#1
dataToSign := createDigestInfo(digest)
// Initialize signing on token
ps.p.SignInit(ps.session, mechanism, ps.keyHandle)
// Sign (happens on hardware)
return ps.p.Sign(ps.session, dataToSign)
}
NSS Signer
type NSSSigner struct {
cert *x509.Certificate
certNSS *C.CERTCertificate
privateKey *C.SECKEYPrivateKey
}
func (n *NSSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
// Call NSS signing function via CGO
rv := C.sign_digest(n.privateKey, digestPtr, digestLen, sigPtr, &sigLen)
// ...
}
Signing Process
Flow
func (s *SignatureService) SignPDFWithProfile(
pdfPath, certFingerprint, pin, profileIDStr string,
) (string, error) {
// 1. Load profile
profile, _ := s.profileManager.GetProfile(profileID)
// 2. Find certificate
certs, _ := s.ListCertificates()
cert := findByFingerprint(certs, certFingerprint)
// 3. Validate certificate
if !cert.IsValid { return error }
if !cert.HasSigningCapability() { return error }
// 4. Route to appropriate signer
switch cert.Source {
case "pkcs11":
return s.signWithPKCS11(pdfPath, cert, pin, profile)
case "pkcs12":
return s.signWithPKCS12(pdfPath, cert, pin, profile)
case "nss":
return s.signWithNSS(pdfPath, cert, pin, profile)
}
}
PDF Signing with pdfsign
func (s *SignatureService) signPDFWithSigner(
pdfPath, outputPath string,
signer CertificateSigner,
cert *types.Certificate,
profile *SignatureProfile,
) error {
// Open input PDF
input, _ := os.Open(pdfPath)
output, _ := os.Create(outputPath)
// Create signature appearance
appearance := CreateSignatureAppearance(profile, cert, time.Now())
// Sign with pdfsign
return sign.Sign(input, output, signer, sign.SignData{
Signature: sign.SignDataSignature{
Info: sign.SignDataSignatureInfo{
Name: cert.Name,
Date: time.Now(),
},
},
Appearance: appearance,
})
}
Signature Appearance
Profile Structure
type SignatureProfile struct {
ID uuid.UUID
Name string
Visibility SignatureVisibility // "invisible" or "visible"
Position SignaturePosition
Appearance SignatureAppearance
IsDefault bool
}
type SignaturePosition struct {
Page int
X float64 // Points from left
Y float64 // Points from bottom
Width float64
Height float64
}
type SignatureAppearance struct {
ShowSignerName bool
ShowSigningTime bool
ShowLocation bool
ShowLogo bool
LogoPath string // Base64 data URL
CustomText string
FontSize int
}
Appearance Generation
// internal/signature/appearance.go
func CreateSignatureAppearance(
profile *SignatureProfile,
cert *types.Certificate,
signingTime time.Time,
) *sign.Appearance {
if profile.Visibility == VisibilityInvisible {
return &sign.Appearance{Visible: false}
}
// Build text lines
var textLines []string
if profile.Appearance.ShowSignerName {
textLines = append(textLines, "Signed by: "+cert.Name)
}
if profile.Appearance.ShowSigningTime {
textLines = append(textLines, "Date: "+signingTime.Format(...))
}
// Generate image
image := generateSignatureImage(textLines, profile)
return &sign.Appearance{
Visible: true,
Page: profile.Position.Page,
LowerLeftX: profile.Position.X,
LowerLeftY: profile.Position.Y,
UpperRightX: profile.Position.X + profile.Position.Width,
UpperRightY: profile.Position.Y + profile.Position.Height,
Image: image,
}
}
Verification
// internal/signature/verification.go
func (s *SignatureService) VerifySignatures(pdfPath string) ([]types.SignatureInfo, error) {
file, _ := os.Open(pdfPath)
// Use pdfsign's verify
response, err := verify.VerifyFile(file)
// Convert to our types
var signatures []types.SignatureInfo
for _, signer := range response.Signers {
sigInfo := types.SignatureInfo{
SignerName: signer.Name,
IsValid: signer.ValidSignature,
CertificateValid: signer.TrustedIssuer,
// ...
}
signatures = append(signatures, sigInfo)
}
return signatures, nil
}
Security Considerations
PIN Handling
PINs stored only in memory during operation
Cleared after signing completes
Never logged or persisted
Path Validation
func validateCertificateStorePath(path string) error {
// Must be absolute
if !filepath.IsAbs(path) { return error }
// Resolve symlinks
resolved, _ := filepath.EvalSymlinks(path)
// Must be in allowed directories
allowedPrefixes := []string{
"/etc/ssl/certs",
homeDir,
// ...
}
// Validate against allowed list
}
PKCS#11 Module Validation
func validatePKCS11Module(modulePath string) error {
// File must exist
// Must be regular file (not symlink, device, etc.)
// Must be readable
// Size within reasonable bounds (1KB - 200MB)
}