Skip to content

Offline TSA timestamps unverifiable: bundle conversion writes bare TimeStampToken where protobuf-specs requires TimeStampResponse #866

Description

@Hammu-admin

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-37SignedData.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-44timestampTokens(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.goSigstoreTimestampingAuthority.Verify calls tsaverification.VerifyTimestampResponse.
  • sigstore/timestamp-authority/pkg/verification/verify.go:VerifyTimestampResponse — calls github.com/digitorus/timestamp.ParseResponse.
  • digitorus/timestamp/timestamp.go:ParseResponseasn1.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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions