sign_core changelog

Copy Markdown View Source

All notable changes are documented here. The format follows Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.1.1] — 2026-05-08

Fixed

  • SignCore.PDF.Reader.read_object_body/2 tolerates a leading whitespace byte before the N N obj header. Microsoft Print To PDF (a very common Windows authoring path) emits xref offsets that point at a single whitespace byte (\n, \r, or space) immediately preceding the object header. Adobe Reader, Poppler, qpdf and mupdf all accept this shape; we rejected it with :object_header_invalid. The bug surfaced on the first business PDF tested against 0.1.0 — a Depósito a Plazo Fijo generated by a Chilean bank — and broke both the verify path (via signature_dicts/1) and the sign path (via Writer's /AcroForm merge through read_catalog_body/2). The fix strips leading PDF whitespace off the slice before matching the header, keeping the regex anchored on the digits — no risk of skipping past whitespace inside a header. Reported and fixed by Eduardo Díaz (#21).

Documentation

  • README cross-package links rewritten to absolute URLs. The published 0.1.0 tarball carried ../ paths to sibling packages (pkcs11ex, soft_signer) and ../docs/specs/specs.md — those don't exist in a single-package tree, so they rendered broken on hexdocs.pm. Now resolved against https://hex.pm/packages/... and https://github.com/utaladriz/pkcs11ex/....
  • @doc redefinition fix in SignCore.XML.Builder. The signature_value/1 @doc block was inserted between an existing @doc and its def signature/N, so the existing docstring was silently overwritten and signature/N shipped to hexdocs.pm with no docs. Reordered so each @doc attaches to its intended def.
  • #{} interpolation fixed in SignCore.XML.XAdES.qualifying_properties/1 docstring. Two un-escaped #{} sequences interpolated to empty in the rendered hexdocs page, leaving cryptic gaps in the option descriptions. Now properly escaped.
  • SignCore.Policy.validate/3 references promoted to callback-link syntax in SignCore.PDF.verify/2 and SignCore.XML.verify/2 moduledocs. ExDoc now auto-resolves the link to the callback definition instead of warning about an unresolved reference.
  • Dropped unused module attributes @ds_local_object and @xades_local_qp from SignCore.XML (left over from an earlier draft of the verify pipeline that referenced them).

[0.1.0] — 2026-05-07

Initial release. Extracted from the pkcs11ex monorepo.

Added — foundational modules

  • SignCore.Signer protocol — pluggable signer abstraction. Implementations carry whatever state is needed to produce a raw signature over arbitrary bytes (a PKCS#11 slot reference, a loaded PKCS#12 bundle, a cloud KMS handle, etc.). The format adapters dispatch via this protocol and don't know about specific provider types.
  • SignCore.PDF — PAdES B-B and B-T sign + verify. 6-step verify pipeline with allowlist-before-math gate, append-attack detection (:incremental_update_after_signature), messageDigest / signature math checks. Hand-rolled CMS encoder over OTP's 'CryptographicMessageSyntax-2009' codec.
  • SignCore.XML — XAdES B-B and B-T sign + verify on top of W3C XML-DSig. Exclusive XML Canonicalization 1.0; <xades:SigningCertificateV2> with RFC 5035 IssuerSerial; XAdES <UnsignedSignatureProperties> for B-T timestamps. Vendored + patched copy of xmerl_c14n (BSD-2) at lib/sign_core/xml/c14n/ — the upstream Hex package crashes on OTP 28's xmlAttribute shapes for unprefixed attributes; the patch is a single fallback clause in do_canonical_name/3, documented inline.
  • SignCore.JWS — RFC 7797 detached + RFC 7515 attached JWS sign + verify with b64: false, crit: ["b64"], and x5c or kid headers.
  • SignCore.CMS — RFC 5652 CMS / SignedData encoding (used by PDF). SignedAttributes, SignedData (with parser), UnsignedAttributes (for B-T id-aa-signatureTimeStampToken), Codec, OIDs, Parsed struct.
  • SignCore.X509 — thin wrapper around OTP's :public_key-decoded X.509 certificates. from_der/1 + cached :spki_sha256 field for SHA-256 SPKI pinning, validity_window/1, check_validity/2.
  • SignCore.Policy — pluggable trust policy behaviour. SignCore.Policy.Allow (test-only) and SignCore.Policy.PinnedRegistry (default — SPKI-pinned allowlist).
  • SignCore.Algorithm — algorithm-adapter behaviour with SignCore.Algorithm.PS256 (RSASSA-PSS / SHA-256 / MGF1-SHA-256 / sLen=32).
  • Telemetry events[:pkcs11ex, :sign | :verify, :start | :stop | :exception] with :format, :alg, :encoding_context, :signer, :byte_count, and on success :subject_id metadata.

Added — verify pipeline

Added — JWS

  • SignCore.JWS.sign/2 :attached opt — produce attached JWS (RFC 7515 form: <header>.<payload_b64>.<sig>) instead of the default detached (RFC 7797 form: <header>..<sig>). When attached, the protected header drops b64/crit and the signing input becomes <header_b64>.<payload_b64> per RFC 7515.
  • SignCore.JWS.sign/2 optional :x5c with kid — when :extra_headers carries a kid, :x5c may be omitted. The header includes kid (RFC 7515 §4.1.4) instead of x5c; verifiers look up the cert by kid.
  • SignCore.JWS.verify/3 auto-detection of attached vs detached. Empty middle segment → detached path; non-empty → attached (extract payload from middle, optionally cross-check against caller-supplied payload arg). Detached without payload returns :missing_payload; attached with mismatched supplied payload returns :payload_mismatch.
  • SignCore.JWS.verify/3 :kid_certs opt%{kid_string => leaf_der} map for kid-based identity resolution. Bypasses policy.resolve/2 (the :kid_certs map IS the operator-supplied allowlist) but still runs policy.validate/3 to derive the subject_id.

Changed

  • SignCore.PDF.sign/2 and SignCore.XML.sign/2 reject missing :alg. Both used to default silently to :PS256; the inconsistency with SignCore.JWS.sign/2 and Pkcs11ex.sign_bytes/2 (which already rejected) let caller typos (:algg) slip through. Now consistent across all four entry points: explicit :alg or {:error, :missing_alg}. Pre-publish breaking change.
  • SignCore.CMS.SignedData.parse/1 disambiguates "issuer found, serial mismatched" from "leaf not in chain". Returns :signer_serial_mismatch for the former (cert rotation without SignerInfo update) vs the prior generic :leaf_certificate_not_found_in_chain.
  • SignCore.XML element / attribute name comparisons handle xmerl's atom + charlist + binary shapes uniformly. Pre-fix, the helpers assumed atoms only; charlist-named attributes silently returned nil, causing downstream :digest_mismatch failures with no clear cause.
  • SignCore.XML.verify/2 no longer raises on malformed base64. The five sites that previously called Base.decode64!/1 (X.509 certs in <ds:KeyInfo>, <ds:SignatureValue>, <xades:CertDigest>, <xades:IssuerSerialV2>, reference digest values) now use Base.decode64/1 and surface tagged errors (:invalid_x5c, :invalid_signature_value, :xades_invalid_cert_digest, :xades_invalid_issuer_serial_v2, :invalid_reference_digest) through the verify pipeline's with chain. Sender-supplied untrusted input must not crash through to telemetry callers.
  • SignCore.XML.sign/2 splice_signature/3 now ignores closing-tag matches inside XML comments and CDATA sections. Previously, the splice picked the LAST </root> substring in the document — a comment legitimately containing </root> would shift the splice onto the wrong byte position. The new path collects byte ranges occupied by <!-- ... --> and <![CDATA[ ... ]]> blocks and rejects matches that fall inside them.
  • SignCore.XML.sign/2 B-T attach path no longer destructures {:ok, _} = ... from Canonicalizer.parse/1 and canonicalize/2. The previous canonical_signature_value/1 would crash on parse / canonicalisation failure; it now propagates the error through the surrounding :bt_failed wrapper.
  • SignCore.PDF.verify/2 now uses SignCore.PDF.Reader to locate the signature dict via the merged xref, replacing the previous regex-over-raw-bytes approach. Walks every revision's xref, takes the newest indirect-object offset per number, scans bodies for /Type /Sig, and parses /ByteRange / /Contents from within the bounded Sig dict body. Tolerates arbitrary PDF whitespace inside the dict (the old regex required exactly one ASCII space) and ignores /ByteRange//Contents text appearing inside content streams, comments, or trailing free text. Behavior change: trailing free text appended after the signed revision that happens to look like a Sig dict now surfaces as :incremental_update_after_signature rather than :multiple_signatures_unsupported_in_v1.
  • SignCore.PDF.verify/2 malformed-CMS handling tightened. The trailing-zero-padding stripper now propagates {:error, :malformed_signature_contents} when /Contents doesn't begin with a SEQUENCE tag, instead of silently passing the malformed bytes to the CMS parser.
  • SignCore.JWS.sign/2 switched to a positive opt-allowlist for signer-forwarded options. Only :signer, :module, :slot_id, :pin, :key_label flow through to Layer 2; new JWS-internal opts no longer leak into the signer pipeline by default.
  • Telemetry :error_class for :missing_x5c / :invalid_x5c / :disallowed_alg is now :input instead of :jws for both SignCore.PDF and SignCore.XML. The previous :jws classification leaked the JWS spec name into PDF and XML telemetry, misattributing format-shared input-validation errors. The atom names themselves (e.g. :missing_x5c) are unchanged.
  • Pkcs11ex.Audit.Anchor.RFC3161.extract_token/1 flattened. The previous with-inside-cond-inside-case-inside-rescue layout is replaced with a single with-pipeline plus two small helpers (check_status/1, extract_tst_tlv/1). Functionally identical; readability is now appropriate for a security-relevant codepath.

Conformance

The shipped output validates under standards-compliant external verifiers:

  • Poppler pdfsig accepts B-B + B-T PDFs.
  • libxmlsec1 xmlsec1 --verify accepts B-B + B-T XML.

Architectural invariants

  • No software signing in this package. sign_core builds the bytes-to-be-signed and assembles the output, but never produces a signature. That's the signer's job.
  • Allowlist before math. Every verify path resolves the sender's certificate against SignCore.Policy before doing any cryptographic verification.
  • Append-attack detection. PAdES verify checks c + d == byte_size(pdf) before parsing the CMS — bytes appended after the signed range are refused.