Module: SAML2::Signable

Included in:
Entity, Entity::Group, Message, Role
Defined in:
lib/saml2/signable.rb

Instance Method Summary collapse

Instance Method Details

#sign(x509_certificate, private_key, algorithm_name = :sha256) ⇒ self

Sign this object.

Parameters:

  • x509_certificate (String)

    The certificate corresponding to private_key, to be embedded in the signature.

  • private_key (String)

    The key to use to sign.

  • algorithm_name (Symbol) (defaults to: :sha256)

Returns:

  • (self)


115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/saml2/signable.rb', line 115

def sign(x509_certificate, private_key, algorithm_name = :sha256)
  to_xml

  xml = @document.root
  xml.set_id_attribute('ID')
  xml.sign!(cert: x509_certificate, key: private_key, digest_alg: algorithm_name.to_s, signature_alg: "rsa-#{algorithm_name}", uri: "##{id}")
  # the Signature element must be the first element
  signature = xml.at_xpath("dsig:Signature", Namespaces::ALL)
  xml.children.first.add_previous_sibling(signature)

  self
end

#signatureNokogiri::XML::Element?

Returns:

  • (Nokogiri::XML::Element, nil)


8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/saml2/signable.rb', line 8

def signature
  unless instance_variable_defined?(:@signature)
    @signature = xml.xpath('//dsig:Signature', Namespaces::ALL).find do |signature|
      signed_node = signature.at_xpath('dsig:SignedInfo/dsig:Reference', Namespaces::ALL)['URI']
      if signed_node == ''
        true if xml == xml.document.root
      elsif signed_node != "##{xml['ID']}"
        false
      else
        # validating the schema will automatically add ID attributes, so check that first
        xml.set_id_attribute('ID') unless xml.document.get_id(xml['ID'])
        true
      end
    end
  end
  @signature
end

#signed?Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/saml2/signable.rb', line 35

def signed?
  !!signature
end

#signing_keyKeyInfo?

Returns:



27
28
29
30
31
32
33
# File 'lib/saml2/signable.rb', line 27

def signing_key
  unless instance_variable_defined?(:@signing_key)
    # don't use `... if signature.at_xpath(...)` - we need to make sure we assign the nil
    @signing_key = (key_info = signature.at_xpath('dsig:KeyInfo', Namespaces::ALL)) ? KeyInfo.from_xml(key_info) : nil
  end
  @signing_key
end

#valid_signature?(**kwargs) ⇒ Boolean

Check if the signature on this object is valid.

Either fingerprint or cert must be provided.

Parameters:

  • key

    optional [String, OpenSSL::PKey::PKey, Array<String>, Array<OpenSSL::PKey::PKey>] Public keys that are allowed to be the valid key.

  • fingerprint

    optional [Array<String>, String] SHA1 fingerprints of trusted certificates. If provided, they will be checked against the #signing_key embedded in the #signature, and if a match is found, the key embedded in the signature will be used for verifying the signature.

  • cert

    optional [Array<String>, String] A single or array of trusted certificates. If provided, they will be checked against the #signing_key embedded in the #signature, and if a match is found, the key embedded in the signature will be used for verifying the signature.

Returns:

  • (Boolean)


102
103
104
# File 'lib/saml2/signable.rb', line 102

def valid_signature?(**kwargs)
  validate_signature(**kwargs).empty?
end

#validate_signature(key: nil, fingerprint: nil, cert: nil) ⇒ Array<String>

Validate the signature on this object.

At least one of key, fingerprint or cert must be provided. If the signature doesn’t specify which key to use, the first provided key will be used.

Parameters:

  • key (defaults to: nil)

    optional [String, OpenSSL::PKey::PKey, Array<String>, Array<OpenSSL::PKey::PKey>] Public keys that are allowed to be the valid key.

  • fingerprint (defaults to: nil)

    optional [Array<String>, String] SHA1 fingerprints of trusted certificates. If provided, they will be checked against the #signing_key embedded in the #signature, and if a match is found, the key embedded in the signature will be used for verifying the signature.

  • cert (defaults to: nil)

    optional [Array<String>, String] A single or array of trusted certificates. If provided, they will be checked against the #signing_key embedded in the #signature, and if a match is found, the key embedded in the signature will be used for verifying the signature.

Returns:

  • (Array<String>)

    An empty array on success, details of errors on failure.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/saml2/signable.rb', line 57

def validate_signature(key: nil,
                       fingerprint: nil,
                       cert: nil)
  return ["not signed"] unless signed?

  certs = Array(cert)
  certs = certs.dup if certs.equal?(cert)
  # see if any given fingerprints match the certificate embedded in the XML;
  # if so, extract the certificate, and add it to the allowed certificates list
  Array(fingerprint).each do |fp|
    certs << signing_key.certificate if signing_key&.fingerprint == SAML2::KeyInfo.format_fingerprint(fp)
  end
  certs = certs.uniq

  trusted_keys = Array.wrap(key).map(&:to_s)
  trusted_keys.concat(certs.map do |cert|
    cert = cert.is_a?(String) ? OpenSSL::X509::Certificate.new(cert) : cert
    cert.public_key.to_s
  end)

  if trusted_keys.include?(signing_key&.public_key&.to_s)
    verification_key = signing_key.public_key.to_s
  end
  # signature doesn't say who signed it. hope and pray it's with the only certificate
  # we know about
  if signing_key.nil?
    verification_key = trusted_keys.first
  end

  return ["no trusted signing key found"] if verification_key.nil?

  begin
    result = signature.verify_with(key: verification_key)
    result ? [] : ["signature is invalid"]
  rescue XMLSec::VerificationError => e
    [e.message]
  end
end