Description
In gitsign v0.17.0, offline signing with GITSIGN_TIMESTAMP_SERVER_URL set produces a CMS id-aa-timeStampToken unsigned attribute whose bytes are a bare RFC 3161 TimeStampToken (a CMS ContentInfo per RFC 5652 §11.4). At verify time, the bundle-conversion layer in internal/sigstore/compat reads those same bytes verbatim into the assembled bundle's RFC3161SignedTimestamp.signed_timestamp protobuf field. But the sigstore protobuf-specs spec that field as "the DER encoded TimeStampResponse" (a different ASN.1 structure — SEQUENCE { PKIStatusInfo, TimeStampToken OPTIONAL }, RFC 3161 §2.4.2).
Downstream, sigstore-go's TSA verifier tries to parse the bytes as TimeStampResp and fails. Every offline-mode gitsign commit that includes a TSA timestamp is unverifiable through the standard verify path.
Reproducer
Sign a commit against a real sigstore/timestamp-authority in offline + sigstore-go mode:
export GITSIGN_ENABLE_SIGSTORE_GO=true
export GITSIGN_REKOR_MODE=offline
export GITSIGN_REKOR_VERSION=2
export GITSIGN_REKOR_URL=http://your-rekor-tiles/
export GITSIGN_FULCIO_URL=http://your-fulcio/
export GITSIGN_TIMESTAMP_SERVER_URL=http://your-tsa/api/v1/timestamp
git commit -S -m "..."
Then verify offline against the standard trust files:
SIGSTORE_ROOT_FILE=fulcio.pem \
SIGSTORE_REKOR_PUBLIC_KEY=rekor-pub.pem \
SIGSTORE_CT_LOG_PUBLIC_KEY_FILE=ctlog-pub.pem \
GITSIGN_TIMESTAMP_CERT_CHAIN=tsa-root.pem \
gitsign verify --certificate-identity=<expected> --certificate-oidc-issuer=<expected> HEAD
Result:
Error: no signer could be verified: signer 0: failed to verify timestamps: threshold not met for verified signed timestamps: 0 < 1; error: unable to verify signed timestamps: error parsing response into Timestamp: asn1: structure error: tags don't match (16 vs {class:0 tag:6 length:9 isCompound:false}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} pkiStatusInfo @2
The pkiStatusInfo @2 error is the giveaway: TimeStampResp expects a SEQUENCE at field position 2 (the PKIStatusInfo), but a bare TimeStampToken has an OBJECT IDENTIFIER there (contentType of the ContentInfo).
Source trace
Sign side (correct per RFC 5652 §11.4 — a bare TimeStampToken in id-aa-timeStampToken):
internal/fork/ietf-cms/timestamp.go:14-37 — SignedData.AddTimestamps(url) fetches TSA response, extracts resp.TimeStampToken, stores it in AttributeTimeStampToken.
internal/signature/sign.go:161-163 and internal/signature/bundlesign.go:85-87 — signing paths that call AddTimestamps.
Verify-side conversion (where the shape mismatch surfaces):
internal/sigstore/compat/attrs.go:29-44 — timestampTokens(si) reads si.UnsignedAttrs.GetValues(oid.AttributeTimeStampToken) and returns the value bytes verbatim.
internal/sigstore/compat/compat.go:146-158 — those bytes are appended to bundle.VerificationMaterial.TimestampVerificationData.Rfc3161Timestamps[*].SignedTimestamp without any shape conversion.
Spec (sigstore/protobuf-specs) — protos/sigstore_common.proto:
message RFC3161SignedTimestamp {
// Signed timestamp is the DER encoded TimeStampResponse.
// See https://www.rfc-editor.org/rfc/rfc3161.html#section-2.4.2
bytes signed_timestamp = 1 [(google.api.field_behavior) = REQUIRED];
}
Downstream verify (correct per spec, so it rejects the bare token):
sigstore-go/pkg/root/timestamping_authority.go — SigstoreTimestampingAuthority.Verify calls tsaverification.VerifyTimestampResponse.
sigstore/timestamp-authority/pkg/verification/verify.go:VerifyTimestampResponse — calls github.com/digitorus/timestamp.ParseResponse.
digitorus/timestamp/timestamp.go:ParseResponse — asn1.Unmarshal(bytes, &resp) against the response struct (SEQUENCE of PKIStatusInfo + optional TimeStampToken). A bare TimeStampToken starts with an OID where PKIStatusInfo expects a SEQUENCE, so this fails at field position 2.
Fix directions
Either:
-
Wrap at bundle-conversion time — in internal/sigstore/compat/attrs.go:timestampTokens, wrap each raw attribute value in a synthetic TimeStampResp (SEQUENCE { SEQUENCE { INTEGER 0 (granted) }, TimeStampToken }) before returning. The added overhead is 5-9 bytes per attribute. Preserves the existing CMS sign-side behavior (which is RFC-compliant), fixes only the bundle field to match its protobuf-specs contract.
-
Capture the full response at sign time — in internal/fork/ietf-cms/timestamp.go:fetchTS and internal/signature/bundlesign.go, embed the whole TSA HTTP response body (which IS a TimeStampResp) into the CMS attribute rather than extracting and storing just the token. This makes the CMS attribute non-compliant with RFC 5652 §11.4 (id-aa-timeStampToken is defined as a bare TimeStampToken), which may break other consumers.
Option 1 seems cleaner and less invasive.
Environment
- gitsign v0.17.0 (bug also present in unreleased HEAD as of 2026-08-06)
- sigstore-go v1.2.2
- cosign v3.1.2
- sigstore/timestamp-authority v2.1.3 (live TSA)
- Rekor v2 (rekor-tiles v2.3.0)
- Fulcio v1.8.8
No workaround available on the verify side — --insecure-ignore-sct doesn't affect timestamp thresholds, and WithObserverTimestamps(1) is unconditionally added to the verifier options in internal/gitsign/bundle.go:369-380 (no CLI or env knob to disable).
Note
We implemented fix direction 1 externally as a wrapper around
gitsign — wrapping the raw attribute value in a synthetic
TimeStampResp before the commit is signed. The resulting
commits verify with stock gitsign verify, offline, with no
network access. Happy to open a PR if the maintainers prefer
that shape.
Description
In gitsign v0.17.0, offline signing with
GITSIGN_TIMESTAMP_SERVER_URLset produces a CMSid-aa-timeStampTokenunsigned attribute whose bytes are a bare RFC 3161TimeStampToken(a CMSContentInfoper RFC 5652 §11.4). At verify time, the bundle-conversion layer ininternal/sigstore/compatreads those same bytes verbatim into the assembled bundle'sRFC3161SignedTimestamp.signed_timestampprotobuf field. But the sigstore protobuf-specs spec that field as "the DER encoded TimeStampResponse" (a different ASN.1 structure —SEQUENCE { PKIStatusInfo, TimeStampToken OPTIONAL }, RFC 3161 §2.4.2).Downstream, sigstore-go's TSA verifier tries to parse the bytes as
TimeStampRespand fails. Every offline-mode gitsign commit that includes a TSA timestamp is unverifiable through the standard verify path.Reproducer
Sign a commit against a real
sigstore/timestamp-authorityin offline + sigstore-go mode:Then verify offline against the standard trust files:
Result:
The
pkiStatusInfo @2error is the giveaway:TimeStampRespexpects aSEQUENCEat field position 2 (the PKIStatusInfo), but a bareTimeStampTokenhas anOBJECT IDENTIFIERthere (contentType of the ContentInfo).Source trace
Sign side (correct per RFC 5652 §11.4 — a bare
TimeStampTokeninid-aa-timeStampToken):internal/fork/ietf-cms/timestamp.go:14-37—SignedData.AddTimestamps(url)fetches TSA response, extractsresp.TimeStampToken, stores it inAttributeTimeStampToken.internal/signature/sign.go:161-163andinternal/signature/bundlesign.go:85-87— signing paths that callAddTimestamps.Verify-side conversion (where the shape mismatch surfaces):
internal/sigstore/compat/attrs.go:29-44—timestampTokens(si)readssi.UnsignedAttrs.GetValues(oid.AttributeTimeStampToken)and returns the value bytes verbatim.internal/sigstore/compat/compat.go:146-158— those bytes are appended tobundle.VerificationMaterial.TimestampVerificationData.Rfc3161Timestamps[*].SignedTimestampwithout any shape conversion.Spec (
sigstore/protobuf-specs) —protos/sigstore_common.proto:Downstream verify (correct per spec, so it rejects the bare token):
sigstore-go/pkg/root/timestamping_authority.go—SigstoreTimestampingAuthority.Verifycallstsaverification.VerifyTimestampResponse.sigstore/timestamp-authority/pkg/verification/verify.go:VerifyTimestampResponse— callsgithub.qkg1.top/digitorus/timestamp.ParseResponse.digitorus/timestamp/timestamp.go:ParseResponse—asn1.Unmarshal(bytes, &resp)against theresponsestruct (SEQUENCE ofPKIStatusInfo+ optionalTimeStampToken). A bareTimeStampTokenstarts with an OID where PKIStatusInfo expects a SEQUENCE, so this fails at field position 2.Fix directions
Either:
Wrap at bundle-conversion time — in
internal/sigstore/compat/attrs.go:timestampTokens, wrap each raw attribute value in a syntheticTimeStampResp(SEQUENCE { SEQUENCE { INTEGER 0 (granted) }, TimeStampToken }) before returning. The added overhead is 5-9 bytes per attribute. Preserves the existing CMS sign-side behavior (which is RFC-compliant), fixes only the bundle field to match its protobuf-specs contract.Capture the full response at sign time — in
internal/fork/ietf-cms/timestamp.go:fetchTSandinternal/signature/bundlesign.go, embed the whole TSA HTTP response body (which IS aTimeStampResp) into the CMS attribute rather than extracting and storing just the token. This makes the CMS attribute non-compliant with RFC 5652 §11.4 (id-aa-timeStampToken is defined as a bareTimeStampToken), which may break other consumers.Option 1 seems cleaner and less invasive.
Environment
No workaround available on the verify side —
--insecure-ignore-sctdoesn't affect timestamp thresholds, andWithObserverTimestamps(1)is unconditionally added to the verifier options ininternal/gitsign/bundle.go:369-380(no CLI or env knob to disable).Note
We implemented fix direction 1 externally as a wrapper around
gitsign — wrapping the raw attribute value in a synthetic
TimeStampRespbefore the commit is signed. The resultingcommits verify with stock
gitsign verify, offline, with nonetwork access. Happy to open a PR if the maintainers prefer
that shape.