saml-kit 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/saml-kit-create-self-signed-certificate +5 -5
- data/lib/saml/kit/builders/response.rb +0 -8
- data/lib/saml/kit/certificate.rb +23 -1
- data/lib/saml/kit/configuration.rb +11 -11
- data/lib/saml/kit/crypto.rb +1 -0
- data/lib/saml/kit/default_registry.rb +25 -0
- data/lib/saml/kit/fingerprint.rb +5 -0
- data/lib/saml/kit/id.rb +5 -0
- data/lib/saml/kit/identity_provider_metadata.rb +14 -0
- data/lib/saml/kit/key_pair.rb +7 -0
- data/lib/saml/kit/logout_request.rb +24 -1
- data/lib/saml/kit/logout_response.rb +2 -0
- data/lib/saml/kit/metadata.rb +70 -3
- data/lib/saml/kit/serializable.rb +20 -1
- data/lib/saml/kit/service_provider_metadata.rb +7 -0
- data/lib/saml/kit/signature.rb +3 -0
- data/lib/saml/kit/signatures.rb +2 -0
- data/lib/saml/kit/templatable.rb +14 -0
- data/lib/saml/kit/template.rb +3 -0
- data/lib/saml/kit/trustable.rb +2 -0
- data/lib/saml/kit/version.rb +1 -1
- data/lib/saml/kit/xml.rb +18 -9
- data/lib/saml/kit/xml_decryption.rb +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 868c7d300e79b706187c739acfc803c0a7d3535c
|
4
|
+
data.tar.gz: 5515dc46cc26271c4420b25ef56d908abf77ca2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b63bbbdfa381acbddac50a1a3cc26a170b0e3215ba060b66a64c900b1731cb6ae7b421fb520f39cb6896883e3b31aa3d7aa718e4ce9815c5a4a1ea0c7d345467
|
7
|
+
data.tar.gz: b684aee57843123632b34b2c8194a7fa6c76f439587954a8b51c96bc23f6ea841ec78e27c548110ae4e0e7c65781055215de592c25894b38f1b4b164ea8ea764
|
@@ -1,9 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'saml/kit'
|
3
3
|
|
4
|
-
puts "Enter
|
5
|
-
|
6
|
-
certificate, private_key = Saml::Kit::SelfSignedCertificate.new(
|
4
|
+
puts "Enter Passphrase:"
|
5
|
+
passphrase = STDIN.read.strip
|
6
|
+
certificate, private_key = Saml::Kit::SelfSignedCertificate.new(passphrase).create
|
7
7
|
|
8
8
|
puts "** BEGIN File Format **"
|
9
9
|
print certificate
|
@@ -18,5 +18,5 @@ puts private_key.inspect
|
|
18
18
|
puts "***********************"
|
19
19
|
|
20
20
|
puts
|
21
|
-
puts "Private Key
|
22
|
-
puts
|
21
|
+
puts "Private Key Passphrase:"
|
22
|
+
puts passphrase.inspect
|
@@ -18,18 +18,10 @@ module Saml
|
|
18
18
|
@version = "2.0"
|
19
19
|
@status_code = Namespaces::SUCCESS
|
20
20
|
@issuer = configuration.issuer
|
21
|
-
@embed_signature = want_assertions_signed
|
22
21
|
@encrypt = encryption_certificate.present?
|
23
22
|
@configuration = configuration
|
24
23
|
end
|
25
24
|
|
26
|
-
def want_assertions_signed
|
27
|
-
request.provider.want_assertions_signed
|
28
|
-
rescue => error
|
29
|
-
Saml::Kit.logger.error(error)
|
30
|
-
nil
|
31
|
-
end
|
32
|
-
|
33
25
|
def build
|
34
26
|
Saml::Kit::Response.new(to_xml, request_id: request.id, configuration: configuration)
|
35
27
|
end
|
data/lib/saml/kit/certificate.rb
CHANGED
@@ -3,33 +3,51 @@ module Saml
|
|
3
3
|
class Certificate
|
4
4
|
BEGIN_CERT=/-----BEGIN CERTIFICATE-----/
|
5
5
|
END_CERT=/-----END CERTIFICATE-----/
|
6
|
-
|
6
|
+
# The use can be `:signing` or `:encryption`
|
7
|
+
attr_reader :use
|
7
8
|
|
8
9
|
def initialize(value, use:)
|
9
10
|
@value = value
|
10
11
|
@use = use.downcase.to_sym
|
11
12
|
end
|
12
13
|
|
14
|
+
# @return [Saml::Kit::Fingerprint] the certificate fingerprint.
|
13
15
|
def fingerprint
|
14
16
|
Fingerprint.new(value)
|
15
17
|
end
|
16
18
|
|
19
|
+
# Returns true if this certificate is for the specified use.
|
20
|
+
#
|
21
|
+
# @param use [Symbol] `:signing` or `:encryption`.
|
22
|
+
# @return [Boolean] true or false.
|
17
23
|
def for?(use)
|
18
24
|
self.use == use.to_sym
|
19
25
|
end
|
20
26
|
|
27
|
+
# Returns true if this certificate is used for encryption.
|
28
|
+
#
|
29
|
+
# return [Boolean] true or false.
|
21
30
|
def encryption?
|
22
31
|
for?(:encryption)
|
23
32
|
end
|
24
33
|
|
34
|
+
# Returns true if this certificate is used for signing.
|
35
|
+
#
|
36
|
+
# return [Boolean] true or false.
|
25
37
|
def signing?
|
26
38
|
for?(:signing)
|
27
39
|
end
|
28
40
|
|
41
|
+
# Returns the x509 form.
|
42
|
+
#
|
43
|
+
# return [OpenSSL::X509::Certificate] the OpenSSL equivalent.
|
29
44
|
def x509
|
30
45
|
self.class.to_x509(value)
|
31
46
|
end
|
32
47
|
|
48
|
+
# Returns the public key.
|
49
|
+
#
|
50
|
+
# @return [OpenSSL::PKey::RSA] the RSA public key.
|
33
51
|
def public_key
|
34
52
|
x509.public_key
|
35
53
|
end
|
@@ -68,6 +86,10 @@ module Saml
|
|
68
86
|
Saml::Kit.logger.warn(error)
|
69
87
|
OpenSSL::X509::Certificate.new(value)
|
70
88
|
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
attr_reader :value
|
71
93
|
end
|
72
94
|
end
|
73
95
|
end
|
@@ -11,20 +11,20 @@ module Saml
|
|
11
11
|
# config.logger = Rails.logger
|
12
12
|
# end
|
13
13
|
#
|
14
|
-
# To specify global configuration it is best to do this in an
|
14
|
+
# To specify global configuration it is best to do this in an initializer
|
15
15
|
# that runs at the start of the program.
|
16
16
|
#
|
17
17
|
# Saml::Kit.configure do |configuration|
|
18
18
|
# configuration.issuer = "https://www.example.com/saml/metadata"
|
19
19
|
# configuration.generate_key_pair_for(use: :signing)
|
20
|
-
# configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"],
|
20
|
+
# configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"], passphrase: ENV['PRIVATE_KEY_PASSPHRASE'], use: :encryption)
|
21
21
|
# end
|
22
22
|
class Configuration
|
23
23
|
# The issuer or entity_id to use.
|
24
24
|
attr_accessor :issuer
|
25
|
-
# The signature method to use when generating signatures (See {
|
25
|
+
# The signature method to use when generating signatures (See {Saml::Kit::Builders::XmlSignature::SIGNATURE_METHODS})
|
26
26
|
attr_accessor :signature_method
|
27
|
-
# The digest method to use when generating signatures (See {
|
27
|
+
# The digest method to use when generating signatures (See {Saml::Kit::Builders::XmlSignature::DIGEST_METHODS})
|
28
28
|
attr_accessor :digest_method
|
29
29
|
# The metadata registry to use for searching for metadata associated with an issuer.
|
30
30
|
attr_accessor :registry
|
@@ -47,19 +47,19 @@ module Saml
|
|
47
47
|
#
|
48
48
|
# @param certificate [String] the x509 certificate with public key.
|
49
49
|
# @param private_key [String] the plain text private key.
|
50
|
-
# @param
|
50
|
+
# @param passphrase [String] the password to decrypt the private key.
|
51
51
|
# @param use [Symbol] the type of key pair, `:signing` or `:encryption`
|
52
|
-
def add_key_pair(certificate, private_key,
|
53
|
-
@key_pairs.push(KeyPair.new(certificate, private_key,
|
52
|
+
def add_key_pair(certificate, private_key, passphrase: '', use: :signing)
|
53
|
+
@key_pairs.push(KeyPair.new(certificate, private_key, passphrase, use.to_sym))
|
54
54
|
end
|
55
55
|
|
56
56
|
# Generates a unique key pair that can be used for signing or encryption.
|
57
57
|
#
|
58
58
|
# @param use [Symbol] the type of key pair, `:signing` or `:encryption`
|
59
|
-
# @param
|
60
|
-
def generate_key_pair_for(use:,
|
61
|
-
certificate, private_key = SelfSignedCertificate.new(
|
62
|
-
add_key_pair(certificate, private_key,
|
59
|
+
# @param passphrase [String] the private key passphrase to use.
|
60
|
+
def generate_key_pair_for(use:, passphrase: SecureRandom.uuid)
|
61
|
+
certificate, private_key = SelfSignedCertificate.new(passphrase).create
|
62
|
+
add_key_pair(certificate, private_key, passphrase: passphrase, use: use)
|
63
63
|
end
|
64
64
|
|
65
65
|
# Return each key pair for a specific use.
|
data/lib/saml/kit/crypto.rb
CHANGED
@@ -2,6 +2,31 @@ module Saml
|
|
2
2
|
module Kit
|
3
3
|
# The default metadata registry is used to fetch the metadata associated with an issuer or entity id.
|
4
4
|
# The metadata associated with an issuer is used to verify trust for any SAML documents that are received.
|
5
|
+
#
|
6
|
+
# You can replace the default registry with your own at startup.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# class OnDemandRegistry
|
11
|
+
# def initialize(original)
|
12
|
+
# @original = original
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def metadata_for(entity_id)
|
16
|
+
# found = @original.metadata_for(entity_id)
|
17
|
+
# return found if found
|
18
|
+
#
|
19
|
+
# @original.register_url(entity_id, verify_ssl: Rails.env.production?)
|
20
|
+
# @original.metadata_for(entity_id)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Saml::Kit.configure do |configuration|
|
25
|
+
# configuration.issuer = ENV['ISSUER']
|
26
|
+
# configuration.registry = OnDemandRegistry.new(configuration.registry)
|
27
|
+
# configuration.logger = Rails.logger
|
28
|
+
# end
|
29
|
+
|
5
30
|
class DefaultRegistry
|
6
31
|
def initialize(items = {})
|
7
32
|
@items = items
|
data/lib/saml/kit/fingerprint.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
3
|
# This generates a fingerprint for an X509 Certificate.
|
4
|
+
#
|
5
|
+
# certificate, _ = Saml::Kit::SelfSignedCertificate.new("password").create
|
6
|
+
#
|
7
|
+
# puts Saml::Kit::Fingerprint.new(certificate).to_s
|
8
|
+
# # B7:AB:DC:BD:4D:23:58:65:FD:1A:99:0C:5F:89:EA:87:AD:F1:D7:83:34:7A:E9:E4:88:12:DD:46:1F:38:05:93
|
4
9
|
class Fingerprint
|
5
10
|
# The OpenSSL::X509::Certificate
|
6
11
|
attr_reader :x509
|
data/lib/saml/kit/id.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
|
+
# This class is used primary for generating ID.
|
4
|
+
#https://www.w3.org/2001/XMLSchema.xsd
|
3
5
|
class Id
|
6
|
+
|
7
|
+
# Generate an ID that conforms to the XML Schema.
|
8
|
+
# https://www.w3.org/2001/XMLSchema.xsd
|
4
9
|
def self.generate
|
5
10
|
"_#{SecureRandom.uuid}"
|
6
11
|
end
|
@@ -2,6 +2,7 @@ module Saml
|
|
2
2
|
module Kit
|
3
3
|
# This class is used to parse the IDPSSODescriptor from a SAML metadata document.
|
4
4
|
#
|
5
|
+
# raw_xml = <<-XML
|
5
6
|
# <?xml version="1.0" encoding="UTF-8"?>
|
6
7
|
# <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_cfa24e2f-0ec0-4ee3-abb8-b2fcfe394c1c" entityID="">
|
7
8
|
# <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
@@ -12,6 +13,19 @@ module Saml
|
|
12
13
|
# <saml:Attribute Name="id"/>
|
13
14
|
# </IDPSSODescriptor>
|
14
15
|
# </EntityDescriptor>
|
16
|
+
# XML
|
17
|
+
#
|
18
|
+
# metadata = Saml::Kit::IdentityProviderMetadata.new(raw_xml)
|
19
|
+
# puts metadata.entity_id
|
20
|
+
#
|
21
|
+
# It can also be used to generate IDP metadata.
|
22
|
+
#
|
23
|
+
# metadata = Saml::Kit::IdentityProviderMetadata.build do |builder|
|
24
|
+
# builder.entity_id = "my-entity-id"
|
25
|
+
# end
|
26
|
+
# puts metadata.to_xml
|
27
|
+
#
|
28
|
+
# For more details on generating metadata see {Saml::Kit::Metadata}.
|
15
29
|
class IdentityProviderMetadata < Metadata
|
16
30
|
def initialize(xml)
|
17
31
|
super("IDPSSODescriptor", xml)
|
data/lib/saml/kit/key_pair.rb
CHANGED
@@ -9,10 +9,17 @@ module Saml
|
|
9
9
|
@private_key = OpenSSL::PKey::RSA.new(private_key, passphrase)
|
10
10
|
end
|
11
11
|
|
12
|
+
# Returns true if the key pair is the designated use.
|
13
|
+
#
|
14
|
+
# @param use [Symbol] Can be either `:signing` or `:encryption`.
|
12
15
|
def for?(use)
|
13
16
|
@use == use
|
14
17
|
end
|
15
18
|
|
19
|
+
# Returns a generated self signed certificate with private key.
|
20
|
+
#
|
21
|
+
# @param use [Symbol] Can be either `:signing` or `:encryption`.
|
22
|
+
# @param passphrase [String] the passphrase to use to encrypt the private key.
|
16
23
|
def self.generate(use:, passphrase: SecureRandom.uuid)
|
17
24
|
certificate, private_key = SelfSignedCertificate.new(passphrase).create
|
18
25
|
new(certificate, private_key, passphrase, use)
|
@@ -1,10 +1,33 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
|
-
# This class
|
3
|
+
# This class can be used to parse a LogoutRequest SAML document.
|
4
|
+
#
|
5
|
+
# document = Saml::Kit::LogoutRequest.new(raw_xml)
|
6
|
+
#
|
7
|
+
# It can also be used to generate a new LogoutRequest.
|
8
|
+
#
|
9
|
+
# document = Saml::Kit::LogoutRequest.build do |builder|
|
10
|
+
# builder.issuer = "issuer"
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# puts document.to_xml(pretty: true)
|
14
|
+
#
|
15
|
+
# See {Saml::Kit::Builders::LogoutRequest} for a list of available settings.
|
16
|
+
#
|
17
|
+
# This class can also be used to generate the correspondong LogoutResponse for a LogoutRequest.
|
18
|
+
#
|
19
|
+
# document = Saml::Kit::LogoutRequest.new(raw_xml)
|
20
|
+
# url, saml_params = document.response_for(binding: :http_post)
|
21
|
+
#
|
22
|
+
# See {#response_for} for more information.
|
4
23
|
class LogoutRequest < Document
|
5
24
|
include Requestable
|
6
25
|
validates_presence_of :single_logout_service, if: :expected_type?
|
7
26
|
|
27
|
+
# A new instance of LogoutRequest
|
28
|
+
#
|
29
|
+
# @param xml [String] The raw xml string.
|
30
|
+
# @param configuration [Saml::Kit::Configuration] the configuration to use.
|
8
31
|
def initialize(xml, configuration: Saml::Kit.configuration)
|
9
32
|
super(xml, name: "LogoutRequest", configuration: configuration)
|
10
33
|
end
|
data/lib/saml/kit/metadata.rb
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
|
+
# The Metadata object can be used to parse an XML string of metadata.
|
4
|
+
#
|
5
|
+
# metadata = Saml::Kit::Metadata.from(raw_xml)
|
6
|
+
#
|
7
|
+
# It can also be used to generate a new metadata string.
|
8
|
+
#
|
9
|
+
# metadata = Saml::Kit::Metadata.build do |builder|
|
10
|
+
# builder.entity_id = "my-issuer"
|
11
|
+
# builder.build_service_provider do |x|
|
12
|
+
# x.add_assertion_consumer_service(assertions_url, binding: :http_post)
|
13
|
+
# x.add_single_logout_service(logout_url, binding: :http_post)
|
14
|
+
# end
|
15
|
+
# builder.build_identity_provider do |x|
|
16
|
+
# x.add_single_sign_on_service(login_url, binding: :http_redirect)
|
17
|
+
# x.add_single_logout_service(logout_url, binding: :http_post)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# puts metadata.to_xml(pretty: true)
|
21
|
+
#
|
22
|
+
# See {Saml::Kit::Builders::ServiceProviderMetadata} and {Saml::Kit::Builders::IdentityProviderMetadata}
|
23
|
+
# for a list of options that can be specified.
|
3
24
|
class Metadata
|
4
25
|
METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
|
5
26
|
include ActiveModel::Validations
|
@@ -12,35 +33,39 @@ module Saml
|
|
12
33
|
validate :must_match_xsd
|
13
34
|
validate :must_have_valid_signature
|
14
35
|
|
15
|
-
attr_reader :
|
16
|
-
attr_accessor :hash_algorithm
|
36
|
+
attr_reader :name
|
17
37
|
|
18
38
|
def initialize(name, xml)
|
19
39
|
@name = name
|
20
40
|
@xml = xml
|
21
|
-
@hash_algorithm = OpenSSL::Digest::SHA256
|
22
41
|
end
|
23
42
|
|
43
|
+
# Returns the /EntityDescriptor/@entityID
|
24
44
|
def entity_id
|
25
45
|
document.find_by("/md:EntityDescriptor/@entityID").value
|
26
46
|
end
|
27
47
|
|
48
|
+
# Returns the supported NameIDFormats.
|
28
49
|
def name_id_formats
|
29
50
|
document.find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
|
30
51
|
end
|
31
52
|
|
53
|
+
# Returns the Organization Name
|
32
54
|
def organization_name
|
33
55
|
document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationName").try(:text)
|
34
56
|
end
|
35
57
|
|
58
|
+
# Returns the Organization URL
|
36
59
|
def organization_url
|
37
60
|
document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationURL").try(:text)
|
38
61
|
end
|
39
62
|
|
63
|
+
# Returns the Company
|
40
64
|
def contact_person_company
|
41
65
|
document.find_by("/md:EntityDescriptor/md:ContactPerson/md:Company").try(:text)
|
42
66
|
end
|
43
67
|
|
68
|
+
# Returns each of the X509 certificates.
|
44
69
|
def certificates
|
45
70
|
@certificates ||= document.find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
|
46
71
|
cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", Xml::NAMESPACES).text
|
@@ -48,14 +73,19 @@ module Saml
|
|
48
73
|
end
|
49
74
|
end
|
50
75
|
|
76
|
+
# Returns the encryption certificates
|
51
77
|
def encryption_certificates
|
52
78
|
certificates.find_all(&:encryption?)
|
53
79
|
end
|
54
80
|
|
81
|
+
# Returns the signing certificates.
|
55
82
|
def signing_certificates
|
56
83
|
certificates.find_all(&:signing?)
|
57
84
|
end
|
58
85
|
|
86
|
+
# Returns each of the service endpoints supported by this metadata.
|
87
|
+
#
|
88
|
+
# @param type [String] the type of service. .E.g. `AssertionConsumerServiceURL`
|
59
89
|
def services(type)
|
60
90
|
document.find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
|
61
91
|
binding = item.attribute("Binding").value
|
@@ -64,19 +94,33 @@ module Saml
|
|
64
94
|
end
|
65
95
|
end
|
66
96
|
|
97
|
+
# Returns a specifing service binding.
|
98
|
+
#
|
99
|
+
# @param binding [Symbol] can be `:http_post` or `:http_redirect`.
|
100
|
+
# @param type [Symbol] can be on the service element like `AssertionConsumerServiceURL`, `SingleSignOnService` or `SingleLogoutService`.
|
67
101
|
def service_for(binding:, type:)
|
68
102
|
binding = Saml::Kit::Bindings.binding_for(binding)
|
69
103
|
services(type).find { |x| x.binding?(binding) }
|
70
104
|
end
|
71
105
|
|
106
|
+
# Returns each of the SingleLogoutService bindings
|
72
107
|
def single_logout_services
|
73
108
|
services('SingleLogoutService')
|
74
109
|
end
|
75
110
|
|
111
|
+
# Returns the SingleLogoutService that matches the specified binding.
|
112
|
+
#
|
113
|
+
# @param binding [Symbol] can be `:http_post` or `:http_redirect`.
|
76
114
|
def single_logout_service_for(binding:)
|
77
115
|
service_for(binding: binding, type: 'SingleLogoutService')
|
78
116
|
end
|
79
117
|
|
118
|
+
# Creates a serialized LogoutRequest.
|
119
|
+
#
|
120
|
+
# @param user [Object] a user object that responds to `name_id_for` and `assertion_attributes_for`.
|
121
|
+
# @param binding [Symbol] can be `:http_post` or `:http_redirect`.
|
122
|
+
# @param relay_state [String] the relay state to have echo'd back.
|
123
|
+
# @return [Array] Returns an array with a url and Hash of parameters to send to the other party.
|
80
124
|
def logout_request_for(user, binding: :http_post, relay_state: nil)
|
81
125
|
builder = Saml::Kit::LogoutRequest.builder(user) do |x|
|
82
126
|
yield x if block_given?
|
@@ -85,30 +129,50 @@ module Saml
|
|
85
129
|
request_binding.serialize(builder, relay_state: relay_state)
|
86
130
|
end
|
87
131
|
|
132
|
+
# Returns the certificate that matches the fingerprint
|
133
|
+
#
|
134
|
+
# @param fingerprint [Saml::Kit::Fingerprint] the fingerprint to search for.
|
135
|
+
# @param use [Symbol] the type of certificates to look at. Can be `:signing` or `:encryption`.
|
136
|
+
# @return [Saml::Kit::Certificate] returns the matching `{Saml::Kit::Certificate}`
|
88
137
|
def matches?(fingerprint, use: :signing)
|
89
138
|
certificates.find do |certificate|
|
90
139
|
certificate.for?(use) && certificate.fingerprint == fingerprint
|
91
140
|
end
|
92
141
|
end
|
93
142
|
|
143
|
+
# Returns the XML document converted to a Hash.
|
94
144
|
def to_h
|
95
145
|
@xml_hash ||= Hash.from_xml(to_xml)
|
96
146
|
end
|
97
147
|
|
148
|
+
# Returns the XML document as a String.
|
149
|
+
#
|
150
|
+
# @param pretty [Symbol] true to return a human friendly version of the XML.
|
98
151
|
def to_xml(pretty: false)
|
99
152
|
document.to_xml(pretty: pretty)
|
100
153
|
end
|
101
154
|
|
155
|
+
# Returns the XML document as a [String].
|
102
156
|
def to_s
|
103
157
|
to_xml
|
104
158
|
end
|
105
159
|
|
160
|
+
# Verifies the signature and data using the signing certificates.
|
161
|
+
#
|
162
|
+
# @param algorithm [OpenSSL::Digest] the digest algorithm to use. E.g. `OpenSSL::Digest::SHA256`
|
163
|
+
# @param signature [String] the signature to verify
|
164
|
+
# @param data [String] the data that is used to produce the signature.
|
165
|
+
# @return [Saml::Kit::Certificate] the certificate that was used to produce the signature.
|
106
166
|
def verify(algorithm, signature, data)
|
107
167
|
signing_certificates.find do |certificate|
|
108
168
|
certificate.public_key.verify(algorithm, signature, data)
|
109
169
|
end
|
110
170
|
end
|
111
171
|
|
172
|
+
# Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
|
173
|
+
#
|
174
|
+
# @param content [String] the raw metadata XML.
|
175
|
+
# @return [Saml::Kit::Metadata] the metadata document or subclass.
|
112
176
|
def self.from(content)
|
113
177
|
hash = Hash.from_xml(content)
|
114
178
|
entity_descriptor = hash["EntityDescriptor"]
|
@@ -121,12 +185,15 @@ module Saml
|
|
121
185
|
end
|
122
186
|
end
|
123
187
|
|
188
|
+
# @!visibility private
|
124
189
|
def self.builder_class
|
125
190
|
Saml::Kit::Builders::Metadata
|
126
191
|
end
|
127
192
|
|
128
193
|
private
|
129
194
|
|
195
|
+
attr_reader :xml
|
196
|
+
|
130
197
|
def document
|
131
198
|
@document ||= Xml.new(xml)
|
132
199
|
end
|
@@ -1,28 +1,47 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
3
|
module Serializable
|
4
|
+
# Base 64 decodes the value.
|
5
|
+
#
|
6
|
+
# @param value [String] the string to base 64 decode.
|
4
7
|
def decode(value)
|
5
8
|
Base64.decode64(value)
|
6
9
|
end
|
7
10
|
|
11
|
+
# Base 64 encodes the value.
|
12
|
+
#
|
13
|
+
# @param value [String] the string to base 64 encode.
|
8
14
|
def encode(value)
|
9
15
|
Base64.strict_encode64(value)
|
10
16
|
end
|
11
17
|
|
18
|
+
# Inflates the value using zlib decompression.
|
19
|
+
#
|
20
|
+
# @param value [String] the value to inflate.
|
12
21
|
def inflate(value)
|
13
22
|
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
14
23
|
inflater.inflate(value)
|
15
24
|
end
|
16
25
|
|
17
|
-
# drop header and checksum as per spec.
|
26
|
+
# Deflate the value and drop the header and checksum as per the SAML spec.
|
27
|
+
# https://en.wikipedia.org/wiki/SAML_2.0#HTTP_Redirect_Binding
|
28
|
+
#
|
29
|
+
# @param value [String] the value to deflate.
|
30
|
+
# @param level [Integer] the level of compression.
|
18
31
|
def deflate(value, level: Zlib::BEST_COMPRESSION)
|
19
32
|
Zlib::Deflate.deflate(value, level)[2..-5]
|
20
33
|
end
|
21
34
|
|
35
|
+
# URL unescape a value
|
36
|
+
#
|
37
|
+
# @param value [String] the value to unescape.
|
22
38
|
def unescape(value)
|
23
39
|
CGI.unescape(value)
|
24
40
|
end
|
25
41
|
|
42
|
+
# URL escape a value
|
43
|
+
#
|
44
|
+
# @param value [String] the value to escape.
|
26
45
|
def escape(value)
|
27
46
|
CGI.escape(value)
|
28
47
|
end
|
@@ -5,24 +5,31 @@ module Saml
|
|
5
5
|
super("SPSSODescriptor", xml)
|
6
6
|
end
|
7
7
|
|
8
|
+
# Returns each of the AssertionConsumerService bindings.
|
8
9
|
def assertion_consumer_services
|
9
10
|
services('AssertionConsumerService')
|
10
11
|
end
|
11
12
|
|
13
|
+
# Returns the AssertionConsumerService for the specified binding.
|
14
|
+
#
|
15
|
+
# @param binding [Symbol] can be either `:http_post` or `:http_redirect`
|
12
16
|
def assertion_consumer_service_for(binding:)
|
13
17
|
service_for(binding: binding, type: 'AssertionConsumerService')
|
14
18
|
end
|
15
19
|
|
20
|
+
# Returns true when the metadata demands that Assertions must be signed.
|
16
21
|
def want_assertions_signed
|
17
22
|
attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
|
18
23
|
return true if attribute.nil?
|
19
24
|
attribute.text.downcase == "true"
|
20
25
|
end
|
21
26
|
|
27
|
+
# @!visibility private
|
22
28
|
def self.builder_class
|
23
29
|
Saml::Kit::Builders::ServiceProviderMetadata
|
24
30
|
end
|
25
31
|
|
32
|
+
# @deprecated Use 'Saml::Kit::Builders::ServiceProviderMetadata'.
|
26
33
|
Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::ServiceProviderMetadata::Builder', 'Saml::Kit::Builders::ServiceProviderMetadata')
|
27
34
|
end
|
28
35
|
end
|
data/lib/saml/kit/signature.rb
CHANGED
@@ -5,17 +5,20 @@ module Saml
|
|
5
5
|
@xml_hash = xml_hash
|
6
6
|
end
|
7
7
|
|
8
|
+
# Returns the embedded X509 Certificate
|
8
9
|
def certificate
|
9
10
|
value = to_h.fetch('KeyInfo', {}).fetch('X509Data', {}).fetch('X509Certificate', nil)
|
10
11
|
return if value.nil?
|
11
12
|
Saml::Kit::Certificate.new(value, use: :signing)
|
12
13
|
end
|
13
14
|
|
15
|
+
# Returns true when the fingerprint of the certificate matches one of the certificates registered in the metadata.
|
14
16
|
def trusted?(metadata)
|
15
17
|
return false if metadata.nil?
|
16
18
|
metadata.matches?(certificate.fingerprint, use: :signing)
|
17
19
|
end
|
18
20
|
|
21
|
+
# Returns the XML Hash.
|
19
22
|
def to_h
|
20
23
|
@xml_hash
|
21
24
|
end
|
data/lib/saml/kit/signatures.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
|
+
# @!visibility private
|
3
4
|
class Signatures # :nodoc:
|
4
5
|
# @!visibility private
|
5
6
|
attr_reader :configuration
|
@@ -10,6 +11,7 @@ module Saml
|
|
10
11
|
@key_pair = configuration.key_pairs(use: :signing).last
|
11
12
|
end
|
12
13
|
|
14
|
+
# @!visibility private
|
13
15
|
def sign_with(key_pair)
|
14
16
|
@key_pair = key_pair
|
15
17
|
end
|
data/lib/saml/kit/templatable.rb
CHANGED
@@ -1,34 +1,46 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
3
|
module Templatable
|
4
|
+
# Can be used to disable embeding a signature.
|
5
|
+
# By default a signature will be embedded if a signing
|
6
|
+
# certificate is available via the configuration.
|
4
7
|
attr_accessor :embed_signature
|
5
8
|
|
9
|
+
# @deprecated Use {#embed_signature=} instead of this method.
|
6
10
|
def sign=(value)
|
7
11
|
Saml::Kit.deprecate("sign= is deprecated. Use embed_signature= instead")
|
8
12
|
self.embed_signature = value
|
9
13
|
end
|
10
14
|
|
15
|
+
# Returns the generated XML document with an XML Digital Signature and XML Encryption.
|
11
16
|
def to_xml(xml: ::Builder::XmlMarkup.new)
|
12
17
|
signatures.complete(render(self, xml: xml))
|
13
18
|
end
|
14
19
|
|
20
|
+
# @!visibility private
|
15
21
|
def signature_for(reference_id:, xml:)
|
16
22
|
return unless sign?
|
17
23
|
render(signatures.build(reference_id), xml: xml)
|
18
24
|
end
|
19
25
|
|
26
|
+
# Allows you to specify which key pair to use for generating an XML digital signature.
|
27
|
+
#
|
28
|
+
# @param key_pair [Saml::Kit::KeyPair] the key pair to use for signing.
|
20
29
|
def sign_with(key_pair)
|
21
30
|
signatures.sign_with(key_pair)
|
22
31
|
end
|
23
32
|
|
33
|
+
# Returns true if an embedded signature is requested and at least one signing certificate is available via the configuration.
|
24
34
|
def sign?
|
25
35
|
embed_signature.nil? ? configuration.sign? : embed_signature && configuration.sign?
|
26
36
|
end
|
27
37
|
|
38
|
+
# @!visibility private
|
28
39
|
def signatures
|
29
40
|
@signatures ||= Saml::Kit::Signatures.new(configuration: configuration)
|
30
41
|
end
|
31
42
|
|
43
|
+
# @!visibility private
|
32
44
|
def encryption_for(xml:)
|
33
45
|
if encrypt?
|
34
46
|
temp = ::Builder::XmlMarkup.new
|
@@ -41,10 +53,12 @@ module Saml
|
|
41
53
|
end
|
42
54
|
end
|
43
55
|
|
56
|
+
# @!visibility private
|
44
57
|
def encrypt?
|
45
58
|
encrypt && encryption_certificate
|
46
59
|
end
|
47
60
|
|
61
|
+
# @!visibility private
|
48
62
|
def render(model, options)
|
49
63
|
Saml::Kit::Template.new(model).to_xml(options)
|
50
64
|
end
|
data/lib/saml/kit/template.rb
CHANGED
data/lib/saml/kit/trustable.rb
CHANGED
@@ -9,6 +9,7 @@ module Saml
|
|
9
9
|
validate :must_be_trusted
|
10
10
|
end
|
11
11
|
|
12
|
+
# Returns true when the document has an embedded XML Signature or has been verified externally.
|
12
13
|
def signed?
|
13
14
|
signature_manually_verified || signature.present?
|
14
15
|
end
|
@@ -19,6 +20,7 @@ module Saml
|
|
19
20
|
xml_hash ? Signature.new(xml_hash) : nil
|
20
21
|
end
|
21
22
|
|
23
|
+
# Returns true when documents is signed and the signing certificate belongs to a known service entity.
|
22
24
|
def trusted?
|
23
25
|
return true if signature_manually_verified
|
24
26
|
return false unless signed?
|
data/lib/saml/kit/version.rb
CHANGED
data/lib/saml/kit/xml.rb
CHANGED
@@ -10,8 +10,6 @@ module Saml
|
|
10
10
|
"samlp": Namespaces::PROTOCOL,
|
11
11
|
}.freeze
|
12
12
|
|
13
|
-
attr_reader :raw_xml, :document
|
14
|
-
|
15
13
|
validate :validate_signatures
|
16
14
|
validate :validate_certificates
|
17
15
|
|
@@ -20,27 +18,31 @@ module Saml
|
|
20
18
|
@document = Nokogiri::XML(raw_xml)
|
21
19
|
end
|
22
20
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
Certificate.to_x509(item.text)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
21
|
+
# Returns the first XML node found by searching the document with the provided XPath.
|
22
|
+
#
|
23
|
+
# @param xpath [String] the XPath to use to search the document
|
30
24
|
def find_by(xpath)
|
31
25
|
document.at_xpath(xpath, NAMESPACES)
|
32
26
|
end
|
33
27
|
|
28
|
+
# Returns all XML nodes found by searching the document with the provided XPath.
|
29
|
+
#
|
30
|
+
# @param xpath [String] the XPath to use to search the document
|
34
31
|
def find_all(xpath)
|
35
32
|
document.search(xpath, NAMESPACES)
|
36
33
|
end
|
37
34
|
|
35
|
+
# Return the XML document as a [String].
|
36
|
+
#
|
37
|
+
# @param pretty [Boolean] return the XML string in a human readable format if true.
|
38
38
|
def to_xml(pretty: true)
|
39
39
|
pretty ? document.to_xml(indent: 2) : raw_xml
|
40
40
|
end
|
41
41
|
|
42
42
|
private
|
43
43
|
|
44
|
+
attr_reader :raw_xml, :document
|
45
|
+
|
44
46
|
def validate_signatures
|
45
47
|
invalid_signatures.flat_map(&:errors).uniq.each do |error|
|
46
48
|
errors.add(error, "is invalid")
|
@@ -69,6 +71,13 @@ module Saml
|
|
69
71
|
end
|
70
72
|
end
|
71
73
|
end
|
74
|
+
|
75
|
+
def x509_certificates
|
76
|
+
xpath = "//ds:KeyInfo/ds:X509Data/ds:X509Certificate"
|
77
|
+
document.search(xpath, Xmldsig::NAMESPACES).map do |item|
|
78
|
+
Certificate.to_x509(item.text)
|
79
|
+
end
|
80
|
+
end
|
72
81
|
end
|
73
82
|
end
|
74
83
|
end
|
@@ -1,12 +1,16 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
3
|
class XmlDecryption
|
4
|
+
# The list of private keys to use to attempt to decrypt the document.
|
4
5
|
attr_reader :private_keys
|
5
6
|
|
6
7
|
def initialize(configuration: Saml::Kit.configuration)
|
7
8
|
@private_keys = configuration.private_keys(use: :encryption)
|
8
9
|
end
|
9
10
|
|
11
|
+
# Decrypts an EncryptedData section of an XML document.
|
12
|
+
#
|
13
|
+
# @param data [Hash] the XML document converted to a [Hash] using Hash.from_xml.
|
10
14
|
def decrypt(data)
|
11
15
|
encrypted_data = data['EncryptedData']
|
12
16
|
symmetric_key = symmetric_key_from(encrypted_data)
|