Class: WSDL::Security::Signature

Inherits:
Object
  • Object
show all
Defined in:
lib/wsdl/security/signature.rb

Overview

Handles XML Digital Signature (XML-DSig) creation for WS-Security.

The Signature class orchestrates the signing process for SOAP messages:

  1. Digests referenced elements (Timestamp, Body, etc.)
  2. Builds the SignedInfo element with References
  3. Canonicalizes and signs the SignedInfo
  4. Builds the complete Signature element with KeyInfo

Examples:

Basic usage

signature = Signature.new(
  certificate: OpenSSL::X509::Certificate.new(cert_pem),
  private_key: OpenSSL::PKey::RSA.new(key_pem, 'password')
)
signature.sign_element(body_node, id: 'Body-123')
signature.sign_element(timestamp_node, id: 'Timestamp-456')
signature.apply(document)

With IssuerSerial key reference

signature = Signature.new(
  certificate: cert,
  private_key: key,
  key_reference: :issuer_serial
)

With explicit namespace prefixes

signature = Signature.new(
  certificate: cert,
  private_key: key,
  explicit_namespace_prefixes: true
)

See Also:

Constant Summary collapse

SigAlg =

Local aliases for constants

Constants::Algorithms::Signature
SecurityNS =

Alias for WS-Security namespace constants.

Returns:

  • (Module)
Constants::NS::Security
SignatureNS =

Alias for XML Signature namespace constants.

Returns:

  • (Module)
Constants::NS::Signature
X509 =

Alias for X.509 token profile constants.

Returns:

  • (Module)
Constants::TokenProfiles::X509
Encoding =

Alias for XML encoding URI constants.

Returns:

  • (Module)
Constants::Encoding
KeyRef =

Alias for supported key reference strategies.

Returns:

  • (Module)
Constants::KeyReference
SIGNATURE_ALGORITHMS =

Signature algorithm configurations

{
  sha1: {
    id: SigAlg::RSA_SHA1,
    digest: 'SHA1'
  },
  sha256: {
    id: SigAlg::RSA_SHA256,
    digest: 'SHA256'
  },
  sha512: {
    id: SigAlg::RSA_SHA512,
    digest: 'SHA512'
  }
}.freeze
DEFAULT_ALGORITHM =

Default digest algorithm

:sha256
DEFAULT_KEY_REFERENCE =

Default key reference method

Constants::KeyReference::BINARY_SECURITY_TOKEN

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(certificate:, private_key:, digest_algorithm: DEFAULT_ALGORITHM, security_token_id: nil, key_reference: DEFAULT_KEY_REFERENCE, explicit_namespace_prefixes: false) ⇒ Signature

Creates a new Signature instance.

Parameters:

  • certificate (OpenSSL::X509::Certificate)

    the X.509 certificate

  • private_key (OpenSSL::PKey::RSA, OpenSSL::PKey::EC)

    the private key

  • digest_algorithm (Symbol) (defaults to: DEFAULT_ALGORITHM)

    the digest algorithm (:sha1, :sha256, :sha512)

  • security_token_id (String, nil) (defaults to: nil)

    the wsu:Id for BinarySecurityToken

  • key_reference (Symbol) (defaults to: DEFAULT_KEY_REFERENCE)

    how to reference the certificate:

    • :binary_security_token (default) - embed certificate in message
    • :issuer_serial - reference by issuer DN and serial number
    • :subject_key_identifier - reference by SKI extension
  • explicit_namespace_prefixes (Boolean) (defaults to: false)

    whether to use explicit ns prefixes

Raises:

  • (ArgumentError)

    if certificate or private key is missing



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/wsdl/security/signature.rb', line 135

def initialize(certificate:, private_key:, digest_algorithm: DEFAULT_ALGORITHM,
               security_token_id: nil, key_reference: DEFAULT_KEY_REFERENCE,
               explicit_namespace_prefixes: false)
  @certificate = certificate or raise ArgumentError, 'certificate is required'
  @private_key = private_key or raise ArgumentError, 'private_key is required'
  @digest_algorithm = digest_algorithm
  @security_token_id = security_token_id || IdGenerator.for('SecurityToken')
  @key_reference = key_reference
  @explicit_namespace_prefixes = explicit_namespace_prefixes
  @references = []

  @canonicalizer = Canonicalizer.new(algorithm: :exclusive_1_0)
  @digester = Digester.new(algorithm: digest_algorithm)
  @xml_helper = XmlBuilderHelper.new(explicit_prefixes: explicit_namespace_prefixes)
  @signature_algorithm = SIGNATURE_ALGORITHMS[digest_algorithm] or
    raise ArgumentError, "Unknown digest algorithm: #{digest_algorithm.inspect}"
end

Instance Attribute Details

#certificateOpenSSL::X509::Certificate (readonly)

Returns the X.509 certificate.

Returns:

  • (OpenSSL::X509::Certificate)


95
96
97
# File 'lib/wsdl/security/signature.rb', line 95

def certificate
  @certificate
end

#digest_algorithmSymbol (readonly)

Returns the digest algorithm symbol.

Returns:

  • (Symbol)


103
104
105
# File 'lib/wsdl/security/signature.rb', line 103

def digest_algorithm
  @digest_algorithm
end

#explicit_namespace_prefixesBoolean (readonly)

Returns whether explicit namespace prefixes are enabled.

Returns:

  • (Boolean)


119
120
121
# File 'lib/wsdl/security/signature.rb', line 119

def explicit_namespace_prefixes
  @explicit_namespace_prefixes
end

#key_referenceSymbol (readonly)

Returns the key reference method.

Returns:

  • (Symbol)


115
116
117
# File 'lib/wsdl/security/signature.rb', line 115

def key_reference
  @key_reference
end

#private_keyOpenSSL::PKey::RSA, OpenSSL::PKey::EC (readonly)

Returns the private key.

Returns:

  • (OpenSSL::PKey::RSA, OpenSSL::PKey::EC)


99
100
101
# File 'lib/wsdl/security/signature.rb', line 99

def private_key
  @private_key
end

#referencesArray<Reference> (readonly)

Returns the references to be signed.

Returns:



111
112
113
# File 'lib/wsdl/security/signature.rb', line 111

def references
  @references
end

#security_token_idString (readonly)

Returns the unique ID for the BinarySecurityToken.

Returns:

  • (String)


107
108
109
# File 'lib/wsdl/security/signature.rb', line 107

def security_token_id
  @security_token_id
end

Instance Method Details

#apply(document, security_node) ⇒ Nokogiri::XML::Document

Applies the signature to the document.

This method builds the complete Signature element including:

  • BinarySecurityToken (X.509 certificate) - if using :binary_security_token
  • SignedInfo with References
  • SignatureValue
  • KeyInfo with appropriate reference type

Parameters:

  • document (Nokogiri::XML::Document)

    the SOAP document

  • security_node (Nokogiri::XML::Node)

    the wsse:Security element

Returns:

  • (Nokogiri::XML::Document)

    the signed document



206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/wsdl/security/signature.rb', line 206

def apply(document, security_node)
  # Build SignedInfo
  signed_info_xml = build_signed_info

  # Canonicalize SignedInfo and compute signature
  canonical_signed_info = @canonicalizer.canonicalize(signed_info_xml)
  signature_value = compute_signature(canonical_signed_info)

  # Build and insert complete Signature element
  build_signature_element(document, security_node, signed_info_xml, signature_value)

  document
end

#clear_referencesself

Clears all references to allow reuse.

Returns:

  • (self)


250
251
252
253
# File 'lib/wsdl/security/signature.rb', line 250

def clear_references
  @references = []
  self
end

#encoded_certificateString

Returns the Base64-encoded DER representation of the certificate.

This is used in the BinarySecurityToken element.

Returns:

  • (String)

    the Base64-encoded certificate



226
227
228
# File 'lib/wsdl/security/signature.rb', line 226

def encoded_certificate
  Base64.strict_encode64(@certificate.to_der)
end

#explicit_namespace_prefixes?Boolean

Returns whether explicit namespace prefixes should be used.

Returns:

  • (Boolean)


242
243
244
# File 'lib/wsdl/security/signature.rb', line 242

def explicit_namespace_prefixes?
  @explicit_namespace_prefixes == true
end

#inspectString

Returns a safe string representation that hides the private key.

This method ensures that private keys are never accidentally exposed in logs, error messages, debugger output, or stack traces.

Examples:

signature = Signature.new(certificate: cert, private_key: key)
signature.inspect
# => '#<WSDL::Security::Signature algorithm=:sha256 ... private_key=[REDACTED] ...>'

Returns:

  • (String)

    a redacted representation safe for logging



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/wsdl/security/signature.rb', line 267

def inspect
  cert_subject = begin
    @certificate.subject.to_s
  rescue StandardError
    'unknown'
  end

  parts = [
    "algorithm=#{@digest_algorithm.inspect}",
    "key_reference=#{@key_reference.inspect}",
    'private_key=[REDACTED]',
    "certificate=#{cert_subject.inspect}",
    "references=#{@references.size}"
  ]

  "#<#{self.class.name} #{parts.join(' ')}>"
end

#references?Boolean

Returns whether any references have been added.

Returns:

  • (Boolean)

    true if there are references to sign



234
235
236
# File 'lib/wsdl/security/signature.rb', line 234

def references?
  !@references.empty?
end

#sign_element(node, id: nil, inclusive_namespaces: nil) ⇒ self Also known as: digest!

Adds an element to the list of elements to be signed.

The element must have a wsu:Id attribute that will be used as the Reference URI. If the element doesn't have an ID, you must provide one.

Parameters:

  • node (Nokogiri::XML::Node)

    the element to sign

  • id (String, nil) (defaults to: nil)

    the wsu:Id value (extracted from node if nil)

  • inclusive_namespaces (Array<String>, nil) (defaults to: nil)

    namespace prefixes for C14N

Returns:

  • (self)

    for method chaining

Raises:

  • (ArgumentError)

    if no ID can be determined



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/wsdl/security/signature.rb', line 166

def sign_element(node, id: nil, inclusive_namespaces: nil)
  element_id = id || extract_id(node)
  raise ArgumentError, 'Element must have a wsu:Id attribute or id must be provided' unless element_id

  # Canonicalize and compute digest
  canonical_xml = @canonicalizer.canonicalize(node, inclusive_namespaces: inclusive_namespaces)
  digest_value = @digester.base64_digest(canonical_xml)

  @references << Reference.new(
    id: element_id,
    digest_value: digest_value,
    inclusive_namespaces: inclusive_namespaces
  )

  self
end