SignCore.JWS (sign_core v0.1.0)

Copy Markdown View Source

JWS format adapter — detached (RFC 7797) by default, attached (RFC 7515) opt-in.

Wire formats

<header_b64u>..<sig_b64u>            # detached (default, RFC 7797)
<header_b64u>.<payload_b64u>.<sig>   # attached (opt-in, RFC 7515)

Detached form sets b64: false + crit: ["b64"] in the protected header and uses the raw payload bytes (not base64url'd) in the signing input. Attached form follows standard RFC 7515 framing — payload is base64url-encoded into the middle segment.

Sign

SignCore.JWS.sign(payload,
  signer: %SomeSigner{...},          # any SignCore.Signer impl
  alg: :PS256,
  x5c: leaf_der_binary,              # or [leaf_der, intermediate_der, ...]
  attached: false,                   # default — detached
  extra_headers: %{"kid" => "platform-1"}   # optional
)

Optional :x5c with :kid

When :extra_headers carries a kid, :x5c becomes optional — the verifier will look up the cert by kid (via :kid_certs opt on verify/3 or a kid-aware policy). RFC 7515 §4.1.4.

Verify

SignCore.JWS.verify(jws, payload, opts \\ [])

Auto-detects detached vs attached from the wire format. For detached, payload is required. For attached, payload may be nil (extracted from the middle segment) or supplied (cross- checked against the embedded payload — :payload_mismatch if they differ).

Verification is software-side via OTP :public_key. No PKCS#11 access needed. Trust resolution flows through SignCore.Policy (see :trust_policy opt) or via :kid_certs for kid-only flows. The signer's certificate from x5c is treated as untrusted input until the policy or kid lookup resolves it (specs.md §7.1).

Summary

Functions

Build a detached JWS over payload and return the wire-format string.

Verify a JWS, detached or attached.

Types

jws()

@type jws() :: binary()

payload()

@type payload() :: iodata()

Functions

sign(payload, opts)

@spec sign(
  payload(),
  keyword()
) :: {:ok, jws()} | {:error, term()}

Build a detached JWS over payload and return the wire-format string.

See the moduledoc for the option surface in Phase 1.

verify(jws, payload \\ nil, opts \\ [])

@spec verify(jws(), payload() | nil, keyword()) :: {:ok, term()} | {:error, term()}

Verify a JWS, detached or attached.

Auto-detects from the wire format:

  • Detached (RFC 7797 — <header>..<sig>): the empty middle segment marks the payload as supplied externally. The payload argument is required.
  • Attached (RFC 7515 — <header>.<payload>.<sig>): the payload is encoded in the middle segment. The payload argument is optional; if supplied, it is cross-checked against the embedded payload (:payload_mismatch if they differ).

Returns {:ok, subject_id} on success.

Identity resolution

By default, the embedded x5c chain is routed through the configured SignCore.Policy (resolve/2 then validate/3). For kid-only JWS (no x5c in the header), supply :kid_certs as a %{kid_string => leaf_der} map to look up the leaf cert by kid directly:

SignCore.JWS.verify(jws, payload, kid_certs: %{"acme-2025" => leaf_der})

When :kid_certs resolves a cert, policy.resolve/2 is bypassed but policy.validate/3 still runs to derive the subject_id.