HankoXml::SignedDocument
Signer#sign and HankoXml.sign do not return a bare Nokogiri::XML::Document. A bare
document’s #to_xml defaults to FORMAT | AS_XML, which reindents the tree and invalidates the
signature that was just computed — a footgun that fires silently, produces valid-looking XML, and is
discovered by the counterparty.
signed = HankoXml.sign(doc, key: key, certificate: cert)
# => #<HankoXml::SignedDocument signatures=1>
signed.to_xml # => String, always AS_XML. Safe.
signed.to_s # => same
signed.to_xml(declaration: false) # => same, minus <?xml …?>
signed.write_to(io) # => streams the safe serialization
signed.bytes # => the String, binary encoding
signed.document # => the underlying Nokogiri::XML::Document,
# for callers who want it. Escape hatch,
# documented as "you own the consequences".
signed.signatures # => NodeSet of <ds:Signature>
signed.valid?(certificate: cert) # => self-check round trip
Read-only delegation
SignedDocument delegates read-only Nokogiri methods (#at_xpath, #xpath, #root, #encoding,
#errors, …) to the wrapped document, so it is a drop-in for reading.
Mutators raise on purpose
It deliberately does not delegate mutators — #add_child, #remove, #content= and friends
raise HankoXml::FrozenSignatureError with a message pointing at #document for callers who
genuinely mean it. Any mutation invalidates the signature; failing loudly at the mutation beats
failing mysteriously at the counterparty.
Nodes returned by the readers are still the live tree, so SignedDocument records its serialization
when it is created. #to_xml, #to_s, #bytes and #write_to raise FrozenSignatureError if the
document has changed since, whether through a reader’s node or through #document. To write out a
document you changed on purpose, use HankoXml.serialize(signed.document).
Without Nokogiri
Signed without Nokogiri (into a template the document already holds), a SignedDocument holds only
the signed bytes. #to_xml, #to_s, #bytes, #write_to, #valid? and #inspect work, and
declaration: false drops the <?xml …?> declaration. #document is nil, and the delegated
readers and #signatures raise HankoXml::NokogiriRequired.
sign vs sign!
sign! still returns the caller’s own Nokogiri::XML::Document, unwrapped, because the caller
already holds that object and wrapping it would be a lie about what they have. The asymmetry:
signis safe by construction — returns aSignedDocument.sign!is in-place, returns the plainNokogiri::XML::Document, and expects the caller to serialize withsave_with: Nokogiri::XML::Node::SaveOptions::AS_XML— see the Nokogiri API guide.