docgate

package module
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 7 Imported by: 0

README

docgate

The document gate: a cheap, deterministic admission check for untrusted document uploads, run before anything expensive or external touches the bytes — storage, rendering, or a remote validation / signing service. It answers one question, "should this file go further?", with a typed reason when the answer is no, so callers return clear, actionable errors instead of opaque upstream failures.

go get github.com/gmb-lib/go-docgate

All decisions are structural: magic bytes, archive shape, signature presence. The gate performs no cryptographic verification (that belongs to a validator such as EU DSS) and never executes or renders content. Bytes are checked in memory; enforce your transport-level body limit and pass the fully-read upload.

Two modes

ModeVerify ModeSigning
For "validate / preserve this signed document" endpoints "sign this file" endpoints
Admits only a signed PDF or a signed, well-formed ASiC-E any format
Extensions allow-list: .pdf .asice .edoc .sce — anything else is ErrUnsupportedType no allow-list, but an extension that claims a checked type must be honest about it
Signature required yes (ErrNoSignature otherwise) no — the file may be getting its first signature; presence is still reported
PDF content must parse; magic at offset 0 (rejects prefixed/polyglot files) same checks when the bytes are PDF
ZIP content must be a strict ASiC-E (mimetype first entry, stored, exact media type) strict ASiC-E → full container checks; a plain ZIP passes opaque (KindZip)
Other content rejected passes opaque (KindOther)

In every mode: filename hygiene (path separators, control characters, bidirectional-override spoofing, length, UTF-8 validity → ErrMalformed) and size caps (per file, plus an optional shared multi-file BudgetErrTooLarge).

Usage

res, err := docgate.Check(docgate.ModeVerify, upload.Filename, data)
switch {
case errors.Is(err, docgate.ErrUnsupportedType): // 422: "not a supported signed-document type"
case errors.Is(err, docgate.ErrNoSignature):     // 422: "this document carries no signature"
case errors.Is(err, docgate.ErrTooLarge):        // 413
case errors.Is(err, docgate.ErrMalformed):
    // 422 — log err: the chain preserves the underlying parser error, so a
    // detector failure is observable instead of reading as a clean verdict.
default:
    _ = res.Kind          // pdf | asice | zip | other
    _ = res.HasSignatures // structural presence (never a crypto verdict)
}

Multi-file requests share a byte budget:

b := docgate.NewBudget(64 << 20)
for _, f := range files {
    if _, err := docgate.Check(docgate.ModeSigning, f.Name, f.Data,
        docgate.WithBudget(b)); err != nil { … }
}

Options: WithMaxBytes (per-file cap; default 25 MB — the common per-file limit of qualified signing services), WithBudget, WithContainerLimits (decompression caps forwarded to the container inspection). A typical service maps these to environment configuration and puts the whole gate behind an on/off flag, so a deployment fronted by an already-gated edge can disable it.

Detection notes

  • PDF signatures are detected by two independent structural detectors — the PDF library's document-info flag and a byte scan for a signature dictionary (/Type /Sig + /ByteRange) — either positive counts. This catches signatures in files the library cannot fully parse (e.g. some externally-produced invisible signatures over incremental updates).
  • ASiC-E shape and signature presence come from go-asice (Sniff + Inspect), including its zip-bomb decompression limits.
  • .edoc / .sce are accepted as national ASiC-E variants.

Scope / non-goals

  • No cryptographic validation, no trust decisions — presence and shape only.
  • No content scanning (anti-virus is a separate, deployment-specific concern).
  • No transport handling — the caller owns HTTP limits and multipart parsing.

Documentation

Overview

Package docgate is the document gate: a cheap, deterministic admission check for untrusted document uploads, run BEFORE anything expensive or external touches the bytes (storage, rendering, or a remote validation / signing service).

It answers one question — "should this file go further?" — with a typed reason when the answer is no, so callers can return clear, actionable errors instead of opaque upstream failures. All decisions are structural: magic bytes, archive shape, signature *presence*. The gate performs no cryptographic verification (that belongs to a validator such as EU DSS) and never executes or renders the content.

Two modes cover the two kinds of boundary:

  • ModeVerify — the admission rule for "validate / preserve this signed document" endpoints: only a signed PDF or a signed, well-formed ASiC-E container passes; everything else is rejected with a reason.
  • ModeSigning — the rule for "sign this file" endpoints: any format is admitted (a file about to be signed need not carry a signature), but content that claims or appears to be PDF or ASiC-E must actually parse as such, and size caps always apply.

The bytes are checked in memory; callers enforce their own transport-level body limits and pass the fully-read upload here.

Index

Constants

View Source
const DefaultMaxBytes int64 = 25 << 20 // 25 MB

DefaultMaxBytes is the per-file size cap applied when no option overrides it. It matches the common per-file limit of qualified signing services.

Variables

View Source
var (
	// ErrUnsupportedType: (verify mode) the file is not one of the supported
	// signed-document types, by extension.
	ErrUnsupportedType = errors.New("docgate: not a supported signed-document type")
	// ErrMalformed: the content does not parse as what it claims or appears
	// to be (broken PDF, broken or non-conformant container, an unsafe
	// filename, or an extension that contradicts the bytes).
	ErrMalformed = errors.New("docgate: malformed document")
	// ErrNoSignature: (verify mode) the document is well-formed but carries no
	// signature — there is nothing to validate or preserve.
	ErrNoSignature = errors.New("docgate: document carries no signature")
	// ErrTooLarge: the file exceeds the per-file cap or the shared budget.
	ErrTooLarge = errors.New("docgate: document exceeds the size limit")
)

Rejection reasons. Wrap-aware: use errors.Is against these sentinels; the wrapped detail (including any underlying parser error) is preserved so the caller can log the cause — a swallowed detector error would be indistinguishable from a clean "unsigned" verdict.

Functions

This section is empty.

Types

type Budget

type Budget struct {
	// contains filtered or unexported fields
}

Budget tracks a shared byte allowance across several files in one request (e.g. a multi-document signing preparation). Create one per request and pass it to each Check call via WithBudget; it is not safe for concurrent use.

func NewBudget

func NewBudget(total int64) *Budget

NewBudget returns a Budget allowing total bytes across all files checked against it.

type Kind

type Kind string

Kind is the detected document type.

const (
	// KindPDF: the bytes are a PDF (magic at offset 0).
	KindPDF Kind = "pdf"
	// KindASiCE: the bytes are a well-formed ASiC-E container.
	KindASiCE Kind = "asice"
	// KindZip: a plain ZIP archive that is not an ASiC-E container. Admitted
	// in signing mode as an opaque format (nothing downstream unzips it).
	KindZip Kind = "zip"
	// KindOther: any other format, admitted opaque in signing mode.
	KindOther Kind = "other"
)

type Mode

type Mode int

Mode selects which admission rule Check applies.

const (
	// ModeVerify admits only a signed PDF or a signed, well-formed ASiC-E
	// container (extensions .pdf / .asice / .edoc / .sce).
	ModeVerify Mode = iota
	// ModeSigning admits any format under the size caps; PDF and ASiC-E
	// content is structurally checked, other content passes opaque.
	ModeSigning
)

type Option

type Option func(*options)

Option tunes a Check call.

func WithBudget

func WithBudget(b *Budget) Option

WithBudget applies a shared multi-file byte budget in addition to the per-file cap.

func WithContainerLimits

func WithContainerLimits(l asice.Limits) Option

WithContainerLimits overrides the decompression limits used when a container is inspected (per-entry / total / entry-count caps; the container library's defaults otherwise).

func WithMaxBytes

func WithMaxBytes(n int64) Option

WithMaxBytes overrides the per-file size cap (DefaultMaxBytes when unset; a non-positive value keeps the default).

type Result

type Result struct {
	// Kind is the detected type.
	Kind Kind
	// HasSignatures reports whether a signature was structurally detected.
	// Authoritative for KindPDF and KindASiCE; always false for opaque kinds.
	HasSignatures bool
}

Result describes an admitted document.

func Check

func Check(mode Mode, filename string, data []byte, opts ...Option) (Result, error)

Check gates one uploaded file. It returns the detected Result when the file is admitted, or an error wrapping one of the package sentinels (ErrUnsupportedType, ErrMalformed, ErrNoSignature, ErrTooLarge) when it must be rejected. The wrapped chain preserves any underlying parser error — log it; a silent detector failure is indistinguishable from a clean verdict.

In every mode the filename is checked first (hygiene is format-independent) and the size caps apply. Beyond that:

  • ModeVerify: the extension must be .pdf / .asice / .edoc / .sce; the bytes must parse as that type (an extension contradicting the bytes is malformed, not re-routed) and must carry at least one signature.
  • ModeSigning: any format is admitted. Content that claims (by extension) or appears (by bytes) to be PDF or ASiC-E must parse as such; a plain ZIP that is not a container passes opaque, as does everything else.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL