saml-kit 0.2.14 → 0.2.15
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|