HankoXml::Signer
signer = HankoXml::Signer.new(
key: key, # see Keys & Certificates
certificate: cert, # optional
certificates: [cert, intermediate], # chain for <X509Data>, optional
signature_method: :rsa_sha256, # default
canonicalization_method: :exc_c14n, # default, applies to <SignedInfo>
digest_method: :sha256, # default for every reference
references: [], # default = [whole document]
key_info: [:x509_certificate], # default
placement: :append_to_root, # default, see Placement
id_attributes: %w[Id ID id],
namespaces: {}, # prefix => href, for XPath args
signature_id: nil, # sets <Signature Id="...">
policy: HankoXml::Policy.strict, # default
template: :adopt, # :adopt | :build | :require
)
Block form
For when the reference list is non-trivial:
signer = HankoXml::Signer.new(key: key, certificate: cert) do |s|
s.signature_method = :rsa_sha1
s.canonicalization_method = :c14n
s.reference "#NFe3520...", transforms: %i[enveloped_signature c14n], digest_method: :sha1
s.key_info :x509_certificate
s.placement = HankoXml::Placement.append_to("/nfe:NFe", namespaces: { "nfe" => NFE_NS })
end
The block runs against a mutable builder; Signer.new freezes the instance before returning it.
A Signer is therefore immutable, reusable, and safe to share across threads.
Exclusive c14n with InclusiveNamespaces (SAML)
xs_aware = HankoXml::Transform.exc_c14n(inclusive_prefixes: %w[xs])
# same as HankoXml::Transform.new(:exc_c14n, inclusive_prefixes: %w[xs])
HankoXml::Signer.new(
key: key, certificate: cert,
policy: HankoXml::Policy.fragment,
placement: HankoXml::Placement.after("//saml:Assertion/saml:Issuer", namespaces: SAML_NS),
references: [HankoXml::Reference.new(uri: "#assertion-1",
transforms: [:enveloped_signature, xs_aware])]
)
This emits <ec:InclusiveNamespaces PrefixList="xs"/> inside the <Transform>. Each listed prefix
(or "#default") is canonicalized the inclusive way: its in-scope declaration is digested even when
it is declared on an ancestor outside the signed element, as SAML’s xsi:type="xs:string" needs.
inclusive_prefixes: on any transform other than :exc_c14n / :exc_c14n_with_comments raises
ConfigurationError.
Verification needs no option. libxmlsec1 reads InclusiveNamespaces from a <Transform> or from
<CanonicalizationMethod>, and the default schema: :default accepts both.
Methods
signer.sign(doc_or_xml) # => HankoXml::SignedDocument, input untouched
signer.sign!(doc) # => the same Nokogiri::XML::Document, now signed
signer.template(doc) # => Nokogiri::XML::Element — the unsigned <Signature>
# it would insert. For inspection and testing.
signer.to_h # => the full effective configuration, resolved URIs
#template exists because “show me exactly what XML you are about to produce, before any crypto
happens” is the single most useful debugging affordance a signing library can offer, and because it
makes the whole template layer testable with plain XML assertions and no keys.
Signing a document that already has a <Signature> template
Many real documents arrive with the <Signature> skeleton already in place — SAML request
templates, ERP envelopes, anything produced by a tool that separates “build the envelope” from
“sign it”. This is a first-class mode:
Signer.new(key: key, template: :adopt) # default
Signer.new(key: key, template: :build) # always build; raise if one exists
Signer.new(key: key, template: :require) # only fill in an existing one; raise if absent
Under :adopt (the default), sign/sign! look for ds:Signature elements first:
- none present → build one from the
Signerconfiguration, as described above; - one or more present → adopt them. The template’s
<SignedInfo>is kept as-is. BeforeNative.sign,policyis enforced on that template (same allowlist as verification) and its Reference URIs must matchreferences. Emptyreferencesmeans whole-documentURI="". A fragment template therefore needsreferences: ["#that-id"]; afile:/// SHA-1 template is rejected underPolicy.strict. Onlykey,certificates,key_infoandid_attributesstill apply to the adopted skeleton.
Without Nokogiri, sign (not sign!) still signs a document that already holds its <Signature>
template, under :adopt or :require, on the bytes: the same policy and reference checks run, and
the result is a bytes-backed SignedDocument. It signs one template per call (pass signature: when
there are several unsigned ones). Building a template, sign! and #template raise
HankoXml::NokogiriRequired.
A template whose Reference URIs do not match references raises HankoXml::ConfigurationError.
A template that violates policy raises HankoXml::PolicyError before any native signing call.
Signing order for multiple/nested signatures
Follows the rule: innermost first, sorted by ancestor depth descending. An outer signature
covering an inner one must be computed after the inner <SignatureValue> exists, or the outer
digest covers an empty element.
signer.sign!(doc) # signs every unsigned <Signature>, innermost first
signer.sign!(doc, force: true) # re-signs even already-signed ones
signer.sign!(doc, signature: 0) # only the nth, document order
force: defaults to false: an already-signed <Signature> (non-empty <SignatureValue>) is left
alone, so signing a partially-signed document is idempotent for the parts already done.
External signers (HSM, cloud KMS, smartcards)
Not supported in v1. The private key does not always exist as bytes you can hand to a library —
AWS KMS, Azure Key Vault, PKCS#11 tokens and Brazilian A3 smartcards all sign on the device. A
future version will keep libxmlsec1 doing all reference resolution, transforms, canonicalization and
digesting (where XML-DSig vulnerabilities actually live), and give it a device-backed key handle
through an OpenSSL provider or PKCS#11 engine, rather than reimplementing that logic in Ruby.
Signer.new raises HankoXml::ConfigurationError if key: is anything but key material or an
HankoXml::Key.