Skip to content

XmlDSigVerifier: verify the enveloped signature of a given element #627

Description

@cjbarth

Problem

node-saml verifies every signature through its own wrapper around SignedXml, getVerifiedXml(fullXml, currentNode, pemFiles): xml.ts#L60-L158. The wrapper adds checks that xml-crypto doesn't make, then returns the bytes the signature covers.

That wrapper is reachable today as @node-saml/node-saml/lib/xml, but it was never public API. node-saml's next major closes lib/ (node-saml/node-saml#458), and it won't promote the wrapper to its root (node-saml/node-saml#441). Anyone verifying signed XML outside a SAML flow therefore needs a replacement. SignedXml's verification path isn't a good destination, because #517 and #519 are replacing it with XmlDSigVerifier. node-saml/node-saml#461 is holding its migration note until it can point there.

At #519's head (e1986cb), the verifier can't yet express what node-saml's wrapper enforces:

Check node-saml's getVerifiedXml XmlDSigVerifier (#519)
Exactly one Signature among the element's children yes Only "one Signature in the whole document", and only when signatureNode is omitted
The signature has exactly one Reference yes No. signedReferences returns every reference, so a caller reading [0] gets whichever came first
That Reference resolves to the signature's own parent yes No
The URI is a same-document reference (#id) yes No (#594)
Transform limit at most 2 per signature maxTransforms, per reference, default 4
No quote in the URI; the ID unique in the document yes yes, from SignedXml
Several pinned certificates, for IdP key rollover tries each publicCert takes one key. truststore takes several, but only through getCertFromKeyInfo, i.e. only when the document's KeyInfo names the certificate

The parent check is the one that matters most. node-saml/node-saml#460 has a fixture that shows why. Take an IdP that signs only its assertions. Move an assertion's Signature, unchanged, from the assertion up to the Response. The signature still verifies, because its reference still resolves to the assertion. Without the parent check, the unsigned Response then passes as signed. signedReferences returns the assertion's bytes, which are authentic, but the caller asked whether the Response was signed. Nothing in the result tells it the answer was "no".

Proposal

Give XmlDSigVerifier a way to verify the enveloped signature of a given element, returning only that element's signed bytes. That is, given an element, verify that:

  1. exactly one Signature is a direct child of the element;
  2. that signature has exactly one Reference;
  3. its URI is # followed by an ID that resolves to exactly one element, the signature's parent;
  4. the transforms stay within maxTransforms;

then return the bytes that reference covers. Each failure should throw its own message, so that a caller can tell a misconfiguration from an attack.

Also let a CertificateKeySelector take several pinned keys, and try each one, without reading KeyInfo. A SAML service provider has to accept an IdP's old and new certificates during a rollover, and must not let the document pick its own key.

XmlDSigVerifier is new API, so item 3 can reject a bare URI from the start. #594 needs a major only because it changes SignedXml. This belongs in 7.0 with #519.

When it ships, node-saml/node-saml#461 can send deep importers of getVerifiedXml() and validateSignature() to it. node-saml's own getVerifiedXml() could then become a thin call to it.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAdds a feature, option or export without breaking existing callers

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions