asice

package module
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 19 Imported by: 0

README

asice

A small, framework-agnostic Go library for assembling and inspecting ASiC-E (.asice) containers that hold XAdES signatures, per ETSI EN 319 162-1 (ASiC) and ETSI EN 319 132-1 (XAdES).

go get github.com/gmb-lib/asice

It is the reusable packaging core. By design it does no network I/O, no authentication, no HTTP, and performs no cryptographic signature verification — that SHALL BE delegated to an external validator (example: EU DSS). The only checks here are over file digests and XAdES references (hashing + XML reading), which keeps the dependency surface to the standard library.

API

Function Purpose
BuildContainer(docs, signatures, opts) ([]byte, error) Assemble a new .asice from 1..N documents + 1..N XAdES signatures.
BuildUnsigned(docs) ([]byte, error) Assemble a signature-less .asice holding 1..N documents (mimetype + data objects + manifest, no signatures*.xml). Fixes the data-object order up front; the first signature is then added like any parallel one (AddSignature / CoSign). Validates names (safe, non-reserved, unique — ErrBadDocumentName).
AddSignature(container, newSignature) ([]byte, error) Add a parallel (co-) signature to an existing .asice — including one with zero signatures so far; derives the next signatures*.xml index itself.
CoSign(original, fileless) ([]byte, error) Add the signature(s) from a fileless (hash-only) container as parallel co-signature(s) on a complete container; one-call ExtractSignatures + AddSignature.
AddDocuments(container, docs) ([]byte, error) Complete a fileless (hash-signed) container by inserting the data objects; verifies filename and digest before inserting.
ExtractSignatures(container) ([]File, error) Return the signatures*.xml entries from a container (including a fileless one); output is suitable for AddSignature.
DataObjects(container) ([]File, error) Return the container-root data objects (signed files) with their bytes; use to recompute the digests a co-signature must reference.
Inspect(container) (Manifest, []SignatureInfo, []DataObject, error) Enumerate manifest, signatures, and data objects.
CheckReferences(docs, signatures) error Verify signatures reference exactly the supplied documents (count + filename + SHA-2 digest).
Sniff(data) error Strict outer-shape check for untrusted bytes: ZIP magic at offset 0 (rejects prefixed/polyglot files) + a mimetype first entry, stored uncompressed, with the exact ASiC-E media type. Shape only — no signature or crypto checks.
IsZip(data) bool ZIP local-file-header magic at offset 0 (byte check only). Combine with Sniff to tell "plain ZIP" apart from "ASiC-E".
Reading untrusted containers — decompression limits

Every container-reading function (Inspect, DataObjects, ExtractSignatures, AddSignature, CoSign, AddDocuments, Sniff) decompresses entries under limits, so a crafted archive (a "zip bomb") cannot exhaust memory: a per-entry cap, a total cap across the operation, and an entry-count cap. Enforcement wraps the readers themselves — the ZIP headers' declared sizes are never trusted. Defaults (DefaultLimits: 64 MiB per entry, 128 MiB total, 512 entries) suit real containers; override per call:

_, _, _, err := asice.Inspect(data, asice.WithLimits(asice.Limits{MaxEntryBytes: 8 << 20}))
// errors.Is(err, asice.ErrTooLarge) / errors.Is(err, asice.ErrTooManyEntries)

A non-positive field in WithLimits keeps its default, so raising one limit never silently disables the others.

Example
docs := []asice.File{
    {Name: "contract.pdf", Data: pdfBytes},
}
sigs := []asice.File{
    {Name: "xades.xml", Data: xadesBytes}, // detached XAdES from CSC / wallet
}

// Optional: validate references explicitly (BuildContainer also does this
// unless BuildOptions.SkipReferenceCheck is set).
if err := asice.CheckReferences(docs, sigs); err != nil {
    log.Fatal(err)
}

container, err := asice.BuildContainer(docs, sigs, nil)
if err != nil {
    log.Fatal(err)
}
// `container` is the raw .asice — hand it to external validation service to validate.

// Later, a second party co-signs the same data objects:
updated, err := asice.AddSignature(container, secondXadesBytes)

What it produces

A standards-shaped ASiC-E ZIP:

  • mimetypefirst entry, stored uncompressed, value application/vnd.etsi.asic-e+zip, written with a clean local header (no data descriptor) so it can be read as the container's leading bytes.
  • the original document(s) in the container root.
  • META-INF/manifest.xml — OpenDocument-style manifest; root file-entry media-type is always the ASiC-E type, then one entry per data object.
  • META-INF/signatures0.xml, signatures1.xml, … — each a XAdESSignatures wrapper around one detached ds:Signature. (No ASiCManifest.xml: for the XAdES profile the per-file references live inside the signature.)

AddSignature copies all existing entries byte-for-byte (preserving compressed bytes and CRC) and only appends a new signature file, so previously valid signatures are never disturbed. The input container is treated as immutable.

National profile: validated-by-construction

The library does not hand-tune profile specifics beyond the manifest rule above. A container is considered conformant once it passes validation through external validation service, EU DSS for example.

Errors

Mismatches return sentinel errors (use errors.Is):

ErrFileCountMismatch, ErrFilenameMismatch, ErrDigestMismatch, ErrSignatureTargetMismatch (parallel signing), ErrMalformedXAdES, ErrUnsupportedDigest, ErrInvalidContainer, ErrNoDocuments, ErrNoSignatures, ErrBadDocumentName (unsigned build name validation), ErrTooLarge / ErrTooManyEntries (decompression limits).

Scope / non-goals

  • No key handling, no signature creation — the signer signs externally (wallet / CSC / own tools).
  • No in-process signature cryptography — validation SHALL BE delegated to external validation service, EU DSS for example.
  • Countersignatures are out of scope (parallel/co-signatures only).

Documentation

Overview

Package asice assembles and inspects ASiC-E (.asice) containers holding XAdES signatures, per ETSI EN 319 162-1 (ASiC) and ETSI EN 319 132-1 (XAdES).

The package is deliberately framework-agnostic: it performs no network I/O, no authentication, and no HTTP. It also performs no cryptographic signature verification — that is delegated to an external validator (EU DSS). The checks here are over file digests and XAdES references only (hashing and XML reading), which keeps the dependency surface tiny.

The core entry points mirror the service design:

  • BuildContainer assembles a new .asice from documents + one or more XAdES signature files.
  • AddSignature adds a parallel (co-) signature to an existing .asice, deriving the next signature file index itself.
  • CoSign merges the signature(s) from a fileless hash-only result into an existing container as parallel co-signature(s).
  • DataObjects returns the container's data-object bytes, used to re-digest the inner files a parallel co-signature must reference.
  • Inspect enumerates the manifest, signatures, and data objects in a container.
  • CheckReferences verifies that signature files reference exactly the supplied documents (count, filename, and SHA-2 digest match).

Index

Constants

View Source
const (
	// MimeType is the ASiC-E container media type. It is the value of the
	// uncompressed "mimetype" entry that must be first in the ZIP.
	MimeType = "application/vnd.etsi.asic-e+zip"
)

Container media type and well-known container paths.

Variables

View Source
var (
	// ErrFileCountMismatch: a signature references a different number of data
	// objects than the documents supplied.
	ErrFileCountMismatch = errors.New("asice: file count mismatch")
	// ErrFilenameMismatch: a supplied filename is not referenced by the
	// signature, or vice versa.
	ErrFilenameMismatch = errors.New("asice: filename mismatch")
	// ErrDigestMismatch: a supplied file's content does not match the digest
	// recorded in the signature.
	ErrDigestMismatch = errors.New("asice: digest mismatch")
	// ErrSignatureTargetMismatch: (parallel signing) the new signature signs
	// different data objects than the container already holds.
	ErrSignatureTargetMismatch = errors.New("asice: signature targets differ from container")
	// ErrMalformedXAdES: a signature file is not parseable as XAdES / contains
	// no ds:Signature.
	ErrMalformedXAdES = errors.New("asice: malformed XAdES signature")
	// ErrUnsupportedDigest: a ds:DigestMethod algorithm is not supported.
	ErrUnsupportedDigest = errors.New("asice: unsupported digest algorithm")
	// ErrInvalidContainer: the input bytes are not a readable ASiC-E container.
	ErrInvalidContainer = errors.New("asice: invalid container")
	// ErrNoSignatures: an operation requires at least one signature but none
	// were supplied.
	ErrNoSignatures = errors.New("asice: no signatures supplied")
	// ErrNoDocuments: an operation requires at least one document but none were
	// supplied.
	ErrNoDocuments = errors.New("asice: no documents supplied")
	// ErrBadDocumentName: a supplied document name is empty, unsafe as a ZIP
	// path, reserved (mimetype or META-INF/*), or duplicated in the set.
	ErrBadDocumentName = errors.New("asice: bad document name")
)

Error codes reported by this package. Wrap-aware: use errors.Is against these sentinels.

View Source
var (
	// ErrTooLarge: an entry's decompressed content, or the decompressed total
	// across all entries read, exceeds the configured limit. Raised while
	// reading, so memory use stays bounded regardless of what the ZIP headers
	// claim.
	ErrTooLarge = errors.New("asice: decompressed content exceeds limit")
	// ErrTooManyEntries: the archive holds more entries than the configured
	// limit allows.
	ErrTooManyEntries = errors.New("asice: too many archive entries")
)

Errors reported by the decompression limits. Wrap-aware: use errors.Is against these sentinels.

View Source
var DefaultLimits = Limits{
	MaxEntryBytes: 64 << 20,
	MaxTotalBytes: 128 << 20,
	MaxEntries:    512,
}

DefaultLimits is applied when no Option overrides it: generous for real containers, fatal for decompression bombs.

Functions

func AddDocuments

func AddDocuments(container []byte, docs []File, opts ...Option) ([]byte, error)

AddDocuments inserts data object(s) into a container that references them but is missing their bytes (e.g. a hash-signed, fileless container), producing a complete .asice. It verifies that each supplied document matches what the container's signatures reference before inserting.

The container is read under the decompression Limits (DefaultLimits unless overridden via opts).

func AddSignature

func AddSignature(container, newSignature []byte, opts ...Option) ([]byte, error)

AddSignature adds a parallel (co-) signature to an existing ASiC-E container and returns a new container. The existing entries — data objects and prior signatures — are copied byte-for-byte so that previously valid signatures are never broken; the container is treated as immutable input.

The new signature must reference the same data objects the container already holds (same filenames and digests), otherwise an error wrapping ErrSignatureTargetMismatch is returned. The next signatures*.xml index is derived internally — the caller never supplies it.

The container is read under the decompression Limits (DefaultLimits unless overridden via opts).

func BuildContainer

func BuildContainer(docs, signatures []File, opts *BuildOptions) ([]byte, error)

BuildContainer assembles a new ASiC-E container from one or more original documents and one or more XAdES signature files, returning the raw .asice bytes.

Unless opts.SkipReferenceCheck is set, it first verifies (via CheckReferences) that the signatures reference exactly the supplied documents. The ZIP is laid out per ETSI EN 319 162-1: an uncompressed "mimetype" entry first, the documents in the container root, then META-INF/manifest.xml and one META-INF/signatures*.xml per signature.

func BuildUnsigned added in v1.6.0

func BuildUnsigned(docs []File) ([]byte, error)

BuildUnsigned assembles a signature-less ASiC-E container from one or more original documents, returning the raw .asice bytes. The result is a valid container holding only data objects — an uncompressed "mimetype" entry first, the documents in the container root in the given order, then META-INF/manifest.xml with one file-entry per document. No META-INF/signatures*.xml is written; signatures are added later via AddSignature (or CoSign), which treats the first signature exactly like any parallel one.

Use it to hold a multi-document set in its final container form before anyone has signed: the data-object order is fixed here and preserved by every later signature merge.

Document names must be bare container-root paths: non-empty, safe (no absolute paths or ".." segments), not "mimetype" or under "META-INF/", and unique within the set — violations return an error wrapping ErrBadDocumentName.

func CheckReferences

func CheckReferences(docs, signatures []File) error

CheckReferences verifies that every supplied signature references exactly the supplied documents: same count, same filenames, and a matching digest for each document (pillar 6.1). It performs no cryptographic signature verification — only hashing and XML reading.

On mismatch it returns an error wrapping one of ErrFileCountMismatch, ErrFilenameMismatch, or ErrDigestMismatch (or ErrMalformedXAdES / ErrUnsupportedDigest while parsing).

func CoSign added in v1.3.0

func CoSign(original, fileless []byte, opts ...Option) ([]byte, error)

CoSign adds the signature(s) carried by a fileless container — the hash-only result a signing service returns when it never held the file bytes — as parallel co-signature(s) on an existing, complete container, returning the updated container. It is the one-call form of ExtractSignatures followed by AddSignature, so the caller never has to crack the fileless result itself.

The co-signature(s) must reference exactly the data objects the original container already holds (same filenames and digests); otherwise an error wrapping ErrSignatureTargetMismatch is returned. The original's data objects and prior signatures are copied byte-for-byte, so previously valid signatures remain valid, and each added signature file is given the next free index.

func Inspect

func Inspect(container []byte, opts ...Option) (Manifest, []SignatureInfo, []DataObject, error)

Inspect enumerates the contents of an ASiC-E container: its parsed manifest, the signatures it holds, and its data objects. It does not validate the container cryptographically.

The manifest and signature entries are decompressed under the Limits (DefaultLimits unless overridden via opts); data-object bytes are not read — only their headers are enumerated.

func IsZip added in v1.5.0

func IsZip(data []byte) bool

IsZip reports whether data begins with the ZIP local-file-header magic at offset 0. It is a byte check only — it does not parse the archive.

func Sniff added in v1.5.0

func Sniff(data []byte, opts ...Option) error

Sniff checks that data has the strict outer shape of an ASiC-E container, per ETSI EN 319 162-1, without reading its documents or signatures:

  • the bytes start with the ZIP magic at offset 0 (no prefixed content);
  • the archive parses as a ZIP within the configured entry limit;
  • the first entry is "mimetype", stored uncompressed, with the exact content "application/vnd.etsi.asic-e+zip".

It returns nil for a well-formed container and an error wrapping ErrInvalidContainer (or ErrTooManyEntries) otherwise. A plain ZIP that is not an ASiC-E container fails the mimetype checks — callers that need to tell the two apart can combine Sniff with IsZip.

Sniff is shape-only: it does not confirm the presence of signatures (use Inspect) and performs no cryptographic verification.

Types

type BuildOptions

type BuildOptions struct {
	// SkipReferenceCheck disables the count/filename/digest verification that
	// BuildContainer runs before assembly. Leave false unless the caller has
	// already validated references via CheckReferences.
	SkipReferenceCheck bool
}

BuildOptions tunes container assembly. The zero value is the recommended default: reference checks run and signatures are wrapped per signature file.

type DataObject

type DataObject struct {
	Name      string
	MediaType string
	Size      int64
}

DataObject describes one container-root data object.

type File

type File struct {
	Name string
	Data []byte
}

File is a named blob: an original document or a signature file. Name is the in-container path (data objects live in the container root, so it is normally a bare filename) and Data is the raw bytes.

func DataObjects added in v1.3.0

func DataObjects(container []byte, opts ...Option) ([]File, error)

DataObjects returns the container-root data objects (the signed files) with their bytes, in container order. The mimetype, anything under META-INF/, and directory entries are excluded. Use it to re-compute the digests a parallel co-signature must reference: a co-signature signs the existing container's inner files, not the container blob as a whole. It performs no signature verification.

It returns ErrInvalidContainer if the bytes are not a readable container, or ErrNoDocuments if the container holds no data objects (e.g. a fileless one).

The container is read under the decompression Limits (DefaultLimits unless overridden via opts).

func ExtractSignatures

func ExtractSignatures(container []byte, opts ...Option) ([]File, error)

ExtractSignatures returns the signatures*.xml entries from a container, including a fileless one. The returned File.Data is suitable to pass to AddSignature.

The container is read under the decompression Limits (DefaultLimits unless overridden via opts).

type Limits added in v1.5.0

type Limits struct {
	// MaxEntryBytes caps the decompressed size of any single entry read.
	MaxEntryBytes int64
	// MaxTotalBytes caps the decompressed size summed across all entries read
	// in one operation.
	MaxTotalBytes int64
	// MaxEntries caps the number of entries the archive may hold.
	MaxEntries int
}

Limits bounds the decompression work done while reading a container, so a crafted archive (a "zip bomb") cannot exhaust memory. Enforcement wraps the entry readers themselves — the ZIP headers' declared sizes are never trusted.

type Manifest

type Manifest struct {
	Entries []ManifestEntry
}

Manifest is the parsed META-INF/manifest.xml (OpenDocument-style), as used by .asice profile.

type ManifestEntry

type ManifestEntry struct {
	FullPath  string // manifest:full-path ("/" for the container root entry)
	MediaType string // manifest:media-type
}

ManifestEntry is one file-entry in META-INF/manifest.xml.

type Option added in v1.5.0

type Option func(*Limits)

Option tunes how a container is read. The container-reading entry points (Inspect, DataObjects, ExtractSignatures, AddSignature, CoSign, AddDocuments, Sniff) accept options; with none given they read under DefaultLimits.

func WithLimits added in v1.5.0

func WithLimits(l Limits) Option

WithLimits overrides the default read limits. A non-positive field keeps its default, so a caller raising one limit does not silently disable the others.

type Reference

type Reference struct {
	URI         string // decoded data-object filename (container-relative)
	Algorithm   string // ds:DigestMethod Algorithm URI
	DigestValue string // ds:DigestValue, base64 as recorded in the signature
}

Reference is a single ds:Reference to an external data object, extracted from a XAdES signature's SignedInfo. Same-document references (URIs beginning with "#", e.g. the SignedProperties or KeyInfo references) are not data objects and are excluded.

type SignatureInfo

type SignatureInfo struct {
	Path       string      // e.g. META-INF/signatures0.xml
	References []Reference // data objects this signature covers
}

SignatureInfo describes one ds:Signature found in a container, with the signature file it lives in and the data objects it references.

Jump to

Keyboard shortcuts

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