Class: Ksef::Auth::Signer

Inherits:
Object
  • Object
show all
Includes:
Xades
Defined in:
lib/ksef/auth/signer.rb

Overview

Produces the XAdES-BES enveloped signature that POST /auth/xades-signature requires (docs/REFERENCE.md §4.3).

The algorithms come from Xades and the XML from SignatureTemplate; what lives here is the part that has to be exactly right — what gets canonicalised, in what order, and over which bytes.

Why this signs a String and returns a String

A digest over "the document" has to match what the verifier computes after parsing the bytes we send. Nokogiri's to_xml pretty-prints on output without adding text nodes to the tree, so an in-memory tree and its serialised form can canonicalise differently — the classic XML-DSig footgun. Signing the serialised bytes and emitting them without reformatting removes the discrepancy entirely. ksef-client-csharp deals with the same problem by setting PreserveWhitespace = true.

Constant Summary collapse

SAVE_OPTIONS =

Emit exactly the bytes that were signed: AS_XML alone excludes FORMAT, so nothing is re-indented. Passing the default would silently invalidate every signature this produces.

Nokogiri::XML::Node::SaveOptions::AS_XML
CLOCK_SKEW_SECONDS =

SigningTime is backdated by a minute, lifted from ksef-client-csharp's CertificateTimeBuffer = TimeSpan.FromMinutes(-1). It is unexplained there but is plainly a clock-skew guard: a signing time fractionally in the future relative to the server's clock invites rejection, and being a minute early costs nothing.

60

Constants included from Xades

Xades::C14N_MODE, Xades::CANONICALIZATION, Xades::DIGEST_METHOD, Xades::DS, Xades::ENVELOPED_SIGNATURE, Xades::SIGNATURE_ID, Xades::SIGNATURE_METHOD, Xades::SIGNED_PROPERTIES_ID, Xades::SIGNED_PROPERTIES_TYPE, Xades::XADES, Xades::XPATH_NAMESPACES

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(certificate:, key:, signing_time: nil, clock_skew: CLOCK_SKEW_SECONDS) ⇒ Signer

Returns a new instance of Signer.

Parameters:

  • certificate (OpenSSL::X509::Certificate) —

    must meet the subject requirements of §4.4

  • key (OpenSSL::PKey::RSA) —

    its private key

  • signing_time (Time, nil) (defaults to: nil) —

    override, for deterministic tests

  • clock_skew (Integer) (defaults to: CLOCK_SKEW_SECONDS) —

    seconds to backdate SigningTime by



44
45
46
47
48
49
50
# File 'lib/ksef/auth/signer.rb', line 44

def initialize(certificate:, key:, signing_time: nil, clock_skew: CLOCK_SKEW_SECONDS)
  @certificate = certificate
  @key = key
  @signing_time = signing_time
  @clock_skew = clock_skew
  verify_key_matches_certificate
end

Instance Attribute Details

#certificate ⇒ Object (readonly)

Returns the value of attribute certificate.



37
38
39
# File 'lib/ksef/auth/signer.rb', line 37

def certificate
  @certificate
end

Instance Method Details

#sign(input, validate: true) ⇒ String

Returns the document with a ds:Signature appended to its root.

Parameters:

  • input (String, #to_xml) —

    the unsigned AuthTokenRequest

  • validate (Boolean) (defaults to: true) —

    check against the auth schema first. On by default: signing is expensive and a malformed document is cheap to catch. It has to happen before signing, because a signed document can never be schema-valid (§14.5) — the schema's sequence has no xsd:any, so the very signature the API demands counts as an unexpected element.

Returns:

  • (String) —

    the document with a ds:Signature appended to its root

Raises:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/ksef/auth/signer.rb', line 60

def sign(input, validate: true)
  xml = input.respond_to?(:to_xml) ? input.to_xml : input.to_s
  document = Nokogiri::XML(xml)
  raise ValidationError, "Cannot sign: the document has no root element" if document.root.nil?

  # `TokenRequest` knows whether its own context type can be meaningfully validated
  # (§14.4: two of the four have defective upstream patterns), and carries the advisory
  # explaining a failure that is not the caller's fault. Delegating to it means a
  # `NipVatUe` or `PeppolId` request fails with the reason rather than a bare schema
  # error — and can still be signed with `validate: false`, which is the resolution
  # §14.4 arrived at.
  validate!(input, xml) if validate

  # Computed before the signature exists, which is exactly what the
  # enveloped-signature transform reproduces for the verifier.
  document.root.add_child(signature_for(digest(document.canonicalize(C14N_MODE))))
  seal(document)
  document.to_xml(save_with: SAVE_OPTIONS)
end