Wssec

Wssec::Signer adds an OASIS WS-Security header with a detached XML-DSig signature to a SOAP envelope — signing the soapenv:Body and a wsu:Timestamp with RSA-SHA256 over exclusive-C14N, exactly as WSS4J-based gateways verify.

It is deliberately vendor-agnostic: the signing key/cert, the token identifier, the exclusive-C14N inclusive prefix list, and the timestamp TTL are all supplied by the caller. The gem carries no keystore/ENV assumptions and no gateway-specific values.

Installation

Path/git dependency (this gem is not published to a public registry):

# Gemfile
gem "wssec", path: "../../gems/wssec"
# or
gem "wssec", git: "https://github.com/SELISEdigitalplatforms/wssec.git"

Usage

require "wssec"

key  = OpenSSL::PKey::RSA.new(File.read("private_key.pem"))   # your signing key
cert = OpenSSL::X509::Certificate.new(File.read("cert.pem"))  # embedded as the BinarySecurityToken

signer = Wssec::Signer.new(
  key:                key,
  cert:               cert,
  key_identifier:     "my-key",           # BST wsu:Id becomes "X509-my-key"
  inclusive_prefixes: %w[wsse wsu soapenv ds], # exclusive-C14N PrefixList (gateway-specific)
  timestamp_ttl:      300                 # seconds until the Timestamp expires (optional)
)

doc = Nokogiri::XML(soap_envelope_string)
signer.sign!(doc, body_id: "Id-#{SecureRandom.hex(16)}")
signed_xml = doc.to_xml

sign! mutates the passed Nokogiri document in place and returns it. Pass now: to pin the timestamp (useful for deterministic tests).

Notes

  • inclusive_prefixes is required — supply the exact prefix list your gateway expects rendered into each signed subtree. There is no default, to avoid baking in any single vendor's assumptions.
  • The digest references use exclusive-C14N with the prefix list; SignedInfo is canonicalized without one. This matches how WSS4J recomputes and verifies, so the signing internals are byte-sensitive — treat them as such.

Alternatives

Before reaching for this gem, check whether an existing library fits — it may save you from owning XML-DSig correctness yourself:

  • akami (1.3.1) — the closest match: builds a wsse:Security header with Timestamp and X.509 signature (Akami::WSSE::Signature / Akami::WSSE::VerifySignature), part of the Savon SOAP ecosystem. Choose it for a batteries-included WSSE layer when you don't need byte-level control over canonicalization or the exclusive-C14N inclusive-prefix list.
  • xmldsig (0.7.0) — a general XML-DSig sign/verify implementation. Not WS-Security aware: you assemble the wsse:Security / BinarySecurityToken / SecurityTokenReference scaffolding yourself. Pairs with nokogiri-signatures for a Nokogiri-friendly interface.
  • xmlsec (libxmlsec1 native bindings) — the most standards-complete option, at the cost of a C system dependency.

Why this gem exists: it targets gateways (e.g. Apache WSS4J) that re-verify the exact canonical bytes. It gives the caller precise control over the inclusive-prefix list and the detached Body + Timestamp references, and deliberately canonicalizes SignedInfo without a prefix list to match how those gateways verify. If you don't need that byte-level fidelity, akami or xmldsig will likely serve you with less code to maintain.

Development

After checking out the repo, run bin/setup to install dependencies, then rake test to run the tests (minitest) or rake for tests + RuboCop. bin/console gives an interactive prompt.

License

Available as open source under the terms of the MIT License.