Skip to content

Sign with GlobalSign DSS

GlobalSign’s Digital Signing Service issues a short-lived signing certificate, called an identity, and signs the digest on request, so a signature handler built on it does three things the KMS handlers do not: it asks for an identity, embeds the OCSP response that came with that identity, and adds a timestamp token. The result is close to a PAdES B-T signature with LTV data in the same revision.

The client comes from github.com/unidoc/globalsign-dss and needs the API key and secret plus the mTLS certificate and key pair for the API endpoint https://emea.api.dss.globalsign.com:8443:

c, err := globalsign.NewClient(apiKey, apiSecret, certFilepath, keyFilepath)

GlobalSign issues the API credentials and the mTLS files with a DSS account. The external signing README lists what the example needs.

Requesting an identity

InitSignature requests an identity, which returns the signing certificate, the CA chain and an OCSP response for the signing certificate:

identity, err := globalsign.DSSService.DSSGetIdentity(gsClient.DSSService, es.ctx, signerName,
    &globalsign.IdentityRequest{SubjectDn: globalsign.SubjectDn{}})

signerName is the key the client caches the identity under, for 10 minutes. The signing request in Sign passes the same signerName, so it signs with the identity fetched here instead of requesting a new certificate. The certificate and CA are PEM, decoded into a chain, put on the handler for later use and also written to the signature’s Cert array.

InitSignature finishes with es.Sign(sig, nil). A nil digest takes the early return in Sign and reserves sigLen zero bytes for the contents without calling the service, so exactly one signing request is made per signature, during appender.Write.

The OCSP response and the timestamp

Because the certificate is minted for this signature, its OCSP response is already at hand and gets embedded as an Adobe revocation attribute rather than fetched again later:

_, err := ocsp.ParseResponseForCert(es.ocsp, certs[0], certs[1])
if err != nil {
    return err
}

siConfig.ExtraSignedAttributes = []pkcs7.Attribute{{
    Type: pkcs7.OIDAttributeAdobeRevocation,
    Value: RevocationInfoArchival{
        Ocsp: []asn1.RawValue{{FullBytes: es.ocsp}},
    },
}}

This only happens when the chain has more than one certificate, since verifying the response needs the issuer. signedData.RequestSignerTimestampToken(0, ...) then adds a timestamp token to the first signer, obtained from the DSS timestamp endpoint rather than a public TSA.

LTV in the same revision

generateSignedFile signs and LTV enables in one write, which means EnableChain: the VRI entry cannot be built in the revision that creates the signature, because its key is derived from the signature contents. The clients are tuned before enabling:

ltv, err := model.NewLTV(appender)
if err != nil {
    return nil, err
}
ltv.CertClient.HTTPClient.Timeout = 30 * time.Second
ltv.OCSPClient.HTTPClient.Timeout = 30 * time.Second
ltv.CRLClient = nil

err = ltv.EnableChain(certChain)

The default timeout on all three clients is 5 seconds per request, which is tight for CRL downloads. Setting CRLClient to nil skips CRLs entirely, and setting OCSPClient to nil would skip OCSP the same way. See LTV in one revision for what EnableChain writes.

Limitations

Signer.Public() returns nil. That works because pkcs7 only needs the signer to sign here, but it means the same crypto.Signer cannot be used with x509.CreateCertificate, unlike the KMS examples.

The signature is padded to sigLen, 16384 in the example. The PKCS7 package carries the signing certificate, the GlobalSign CA chain, the OCSP response and a timestamp token with its TSA certificates, which together can exceed 8192 bytes. Sign returns an error if the package does not fit, so raise sigLen when you see that error.

signerName is only the cache key for the identity, and the name and location in SignOption are display strings from the GlobalSign test account. Replace the name and location with your own.

Run the example

NewGlobalSignPdfSignature builds the handler, createSignatureField turns a SignOption into the field and appearance, and generateSignedFile does the signing, LTV and write.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/external
go run pdf_sign_external_globalsign.go <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> <API_KEY> <API_SECRET> <CERT_FILE_PATH> <KEY_FILE_PATH>

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on