mdqt 0.3.1 → 0.4.0
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/.gitignore +1 -0
- data/CHANGELOG.md +13 -0
- data/README.md +24 -2
- data/exe/mdqt +13 -0
- data/lib/mdqt/cli.rb +1 -0
- data/lib/mdqt/cli/base.rb +6 -2
- data/lib/mdqt/cli/check.rb +82 -0
- data/lib/mdqt/cli/get.rb +9 -0
- data/lib/mdqt/client.rb +14 -6
- data/lib/mdqt/client/metadata_file.rb +89 -0
- data/lib/mdqt/client/metadata_response.rb +14 -1
- data/lib/mdqt/client/metadata_service.rb +7 -2
- data/lib/mdqt/client/metadata_validator.rb +29 -0
- data/lib/mdqt/schema/saml-schema-assertion-2.0.xsd +283 -0
- data/lib/mdqt/schema/saml-schema-metadata-2.0.xsd +337 -0
- data/lib/mdqt/schema/xenc-schema.xsd +136 -0
- data/lib/mdqt/schema/xml.xsd +287 -0
- data/lib/mdqt/schema/xmldsig-core-schema.xsd +309 -0
- data/lib/mdqt/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7fbccd299cf9c1a2432b72d55bd74c8c35633529a42137b4ebf82c020a3407d2
|
4
|
+
data.tar.gz: 954092c1deb38ba21374b19bbd00bc2412e92508e95eeda2b7ebb6ada9916ba4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 064fc97f1e597b7d14febeb892f3176e142bcba1b224bf8e46453d1ead108becb2915788e285adf7f5089c4755a28d1d4a4a4ad92a1e580b2b0e2083042deb89
|
7
|
+
data.tar.gz: 476527f791f3d62efae084ad84632131657383bb7697c6a575f77b12ee6054a1fcbd3c91ec29cd1f304a36f32c67e6522a73281da9d6095f8f9dab2a901b60cb
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.4.0
|
4
|
+
|
5
|
+
### New Features
|
6
|
+
- The `check` command will validate XML files against SAML metadata schema and verify signatures
|
7
|
+
- A `--validate` switch for `get` forces XML validation, using basic SAML2 metadata schema
|
8
|
+
- A `--tls-risky` switch for `get` disables verification of TLS certificates
|
9
|
+
|
10
|
+
### Improvements
|
11
|
+
- Connection failures now show an explanation (such as TLS problems)
|
12
|
+
|
13
|
+
### Fixes
|
14
|
+
- "Not Required" was shown when using commands that don't interact with an MDQ server
|
15
|
+
|
3
16
|
## 0.3.1
|
4
17
|
|
5
18
|
### Fixes
|
data/README.md
CHANGED
@@ -9,6 +9,7 @@ MDQ currently supports:
|
|
9
9
|
|
10
10
|
- Downloading single entities, lists or aggregates
|
11
11
|
- Signature verification
|
12
|
+
- Validating metadata against SAML2 schema
|
12
13
|
- Saving metadata to disk
|
13
14
|
- Caching entity metadata on disk
|
14
15
|
- Gzip compression
|
@@ -33,7 +34,7 @@ To install system-wide on your default Ruby, use
|
|
33
34
|
|
34
35
|
$ sudo gem install mdqt
|
35
36
|
|
36
|
-
If using a per-user Ruby via `rbenv` or similar, you'll
|
37
|
+
If using a per-user Ruby via `rbenv` or similar, you'll need
|
37
38
|
|
38
39
|
$ gem install mdqt
|
39
40
|
|
@@ -49,6 +50,11 @@ and then execute:
|
|
49
50
|
|
50
51
|
$ bundle
|
51
52
|
|
53
|
+
### As a Docker container
|
54
|
+
|
55
|
+
(Experimental)
|
56
|
+
See the instructions at [MDQT-Container](https://github.com/Digital-Identity-Labs/mdqt-container)
|
57
|
+
|
52
58
|
### Extra steps for verifying signed metadata
|
53
59
|
|
54
60
|
MDQT can check that metadata has not been tampered with by verifying its
|
@@ -138,6 +144,22 @@ It's possible to pass more than one certificate by separating them with commas
|
|
138
144
|
|
139
145
|
$ mdqt get --verify-with myfederation.pem,previous.pem https://indiid.net/idp/shibboleth
|
140
146
|
|
147
|
+
Basic XML correctness and validation against SAML2 Metadata schema can be enabled with the
|
148
|
+
`--validate` switch:
|
149
|
+
|
150
|
+
$ mdqt get --validate https://indiid.net/idp/shibboleth
|
151
|
+
|
152
|
+
If you need to check metadata that has already been downloaded then try the `check`
|
153
|
+
command:
|
154
|
+
|
155
|
+
$ mdqt check metadata.xml # Just validate
|
156
|
+
$ mdqt check --verify-with myfederation.pem metadata.xml # Verify signature too
|
157
|
+
|
158
|
+
You shouldn't need to *validate* XML from a trusted MDQ service such as one run by a
|
159
|
+
national federation. You should however always *verify* the signature of XML sent over an unencrypyted HTTP connection,
|
160
|
+
and probably even over HTTPS. MDQT's validation check is mostly for use when writing
|
161
|
+
or debugging your own MDQ service.
|
162
|
+
|
141
163
|
### Saving metadata as files
|
142
164
|
|
143
165
|
The simplest way to save metadata is to redirect output from the `get` command:
|
@@ -163,7 +185,7 @@ For more information about current settings, download results, and so on, add
|
|
163
185
|
|
164
186
|
$mdqt get --verbose http://entity.ac.uk/shibboleth
|
165
187
|
|
166
|
-
To convert normal URI entity IDs into MDQ SHA1 hashed transformed identifiers
|
188
|
+
To convert normal URI entity IDs into MDQ SHA1 hashed transformed identifiers use the `transform` command:
|
167
189
|
|
168
190
|
$ mdqt transform http://example.org/service
|
169
191
|
|
data/exe/mdqt
CHANGED
@@ -30,9 +30,11 @@ Commander.configure do
|
|
30
30
|
c.option '--service URL', String, 'MDQ service to search for entities. Defaults to MDQT_SERVICE or MDQ_BASE_URL env variables'
|
31
31
|
c.option '--cache', "Cache downloads and try to fetch from cache where appropriate"
|
32
32
|
c.option '--verify-with PATHS', Array, 'Validate downloads using specified certificates'
|
33
|
+
c.option '--validate', 'Validate downloaded metadata against SAML2 schema (not normally needed)'
|
33
34
|
#c.option '--stdin', 'accept one or more entity ids from STDIN'
|
34
35
|
c.option '--all', 'Request all entity records'
|
35
36
|
c.option '--explain', 'Show details of client request and server response'
|
37
|
+
c.option '--tls-risky', "Don't check certificate used for TLS (usually a bad idea)"
|
36
38
|
c.option '--save-to PATH', String, 'Write all data to files in the specified directory'
|
37
39
|
c.option '--link-id', 'If saving files, save files with aliases (requires `--save-to`)'
|
38
40
|
c.action do |args, options|
|
@@ -62,5 +64,16 @@ Commander.configure do
|
|
62
64
|
end
|
63
65
|
end
|
64
66
|
|
67
|
+
command :check do |c|
|
68
|
+
c.syntax = 'mdqt check XML_FILENAME CERTIFICATE_FILENAME'
|
69
|
+
c.description = 'Validate XML and check signatures'
|
70
|
+
c.option '--verify-with PATHS', Array, 'Validate file using specified certificates'
|
71
|
+
c.action do |args, options|
|
72
|
+
options.default MDQT::CLI::Defaults.cli_defaults
|
73
|
+
options.default({service: :not_required, validate: true })
|
74
|
+
MDQT::CLI::Check.run(args, options)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
65
78
|
end
|
66
79
|
|
data/lib/mdqt/cli.rb
CHANGED
data/lib/mdqt/cli/base.rb
CHANGED
@@ -4,6 +4,7 @@ module MDQT
|
|
4
4
|
|
5
5
|
class Base
|
6
6
|
|
7
|
+
require 'mdqt/cli'
|
7
8
|
require 'pastel'
|
8
9
|
|
9
10
|
def self.run(args, options)
|
@@ -34,11 +35,14 @@ module MDQT
|
|
34
35
|
def self.introduce(args, options)
|
35
36
|
if options.verbose
|
36
37
|
STDERR.puts "MDQT version #{MDQT::VERSION}"
|
37
|
-
STDERR.puts "Using #{options.service}"
|
38
|
+
STDERR.puts "Using #{options.service}" unless options.service == :not_required
|
38
39
|
STDERR.puts "Caching is #{options.cache ? 'on' : 'off'}"
|
40
|
+
STDERR.print "XML validation is #{MDQT::Client.verification_available? ? 'available' : 'not available'}"
|
41
|
+
STDERR.puts " #{options.validate ? "and active" : "but inactive"} for this request" if MDQT::Client.verification_available?
|
39
42
|
STDERR.print "Signature verification is #{MDQT::Client.verification_available? ? 'available' : 'not available'}"
|
40
43
|
STDERR.puts " #{options.verify_with ? "and active" : "but inactive"} for this request" if MDQT::Client.verification_available?
|
41
44
|
STDERR.puts "Output directory for saved files is: #{File.absolute_path(options.save_to)}" if options.save_to
|
45
|
+
STDERR.puts("Warning! TLS certificate verification has been disabled!") if options.tls_risky
|
42
46
|
STDERR.puts
|
43
47
|
end
|
44
48
|
end
|
@@ -104,7 +108,7 @@ module MDQT
|
|
104
108
|
end
|
105
109
|
|
106
110
|
def advise_on_xml_signing_support
|
107
|
-
hey "XML signature validation
|
111
|
+
hey "XML signature verification and XML validation are not available. Install the 'xmldsig' gem if you can." unless MDQT::Client.verification_available?
|
108
112
|
end
|
109
113
|
|
110
114
|
def extract_certificate_paths(cert_paths = options.verify_with)
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module MDQT
|
2
|
+
|
3
|
+
class CLI
|
4
|
+
|
5
|
+
require 'mdqt/cli/base'
|
6
|
+
|
7
|
+
class Check < Base
|
8
|
+
|
9
|
+
def run
|
10
|
+
|
11
|
+
options.validate = true
|
12
|
+
|
13
|
+
advise_on_xml_signing_support
|
14
|
+
halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
|
15
|
+
|
16
|
+
client = MDQT::Client.new(
|
17
|
+
options.service,
|
18
|
+
verbose: options.verbose,
|
19
|
+
explain: options.explain ? true : false,
|
20
|
+
)
|
21
|
+
|
22
|
+
cert_paths = options.verify_with ? extract_certificate_paths(options.verify_with) : []
|
23
|
+
|
24
|
+
args.each do |filename|
|
25
|
+
|
26
|
+
filename = File.absolute_path(filename)
|
27
|
+
file = client.open_metadata(filename)
|
28
|
+
|
29
|
+
halt!("Cannot access file #{filename}") unless file.readable?
|
30
|
+
|
31
|
+
halt!("XML validation failed for #{filename}:\n#{file.validation_error}") unless file.valid?
|
32
|
+
btw"File #{filename} is valid SAML Metadata XML"
|
33
|
+
|
34
|
+
|
35
|
+
if options.verify_with
|
36
|
+
halt! "XML in #{filename} is not signed, cannot verify!" unless file.signed?
|
37
|
+
halt! "The signed XML for #{filename} cannot be verified using #{cert_paths.to_sentence}" unless file.verified_signature?(cert_paths)
|
38
|
+
btw "Signed XML for #{filename} has been verified using '#{cert_paths.to_sentence}'"
|
39
|
+
end
|
40
|
+
|
41
|
+
yay "#{filename} OK"
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def verify_results(results)
|
49
|
+
|
50
|
+
# if options.validate
|
51
|
+
# results.each do |result|
|
52
|
+
# next unless result.ok?
|
53
|
+
# halt! "The data for #{result.identifier} is not valid when checked against schema:\n#{result.validation_error}" unless result.valid?
|
54
|
+
# btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been validated against schema" ## FIXME - needs constistent #label maybe?
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# return results unless options.verify_with
|
59
|
+
#
|
60
|
+
# cert_paths = extract_certificate_paths(options.verify_with)
|
61
|
+
#
|
62
|
+
# results.each do |result|
|
63
|
+
# next unless result.ok?
|
64
|
+
# halt! "Data from #{options.service} is not signed, cannot verify!" unless result.signed?
|
65
|
+
# halt! "The data for #{result.identifier} cannot be verified using #{cert_paths.to_sentence}" unless result.verified_signature?(cert_paths)
|
66
|
+
# btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been verified using '#{cert_paths.to_sentence}'" ## FIXME - needs constistent #label maybe?
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# results
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
|
data/lib/mdqt/cli/get.rb
CHANGED
@@ -26,6 +26,7 @@ module MDQT
|
|
26
26
|
options.service,
|
27
27
|
verbose: options.verbose,
|
28
28
|
explain: options.explain ? true : false,
|
29
|
+
tls_risky: options.tls_risky ? true : false,
|
29
30
|
cache_type: options.cache ? :file : :none,
|
30
31
|
)
|
31
32
|
|
@@ -35,6 +36,14 @@ module MDQT
|
|
35
36
|
|
36
37
|
def verify_results(results)
|
37
38
|
|
39
|
+
if options.validate
|
40
|
+
results.each do |result|
|
41
|
+
next unless result.ok?
|
42
|
+
halt! "The data for #{result.identifier} is not valid when checked against schema:\n#{result.validation_error}" unless result.valid?
|
43
|
+
btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been validated against schema" ## FIXME - needs constistent #label maybe?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
38
47
|
return results unless options.verify_with
|
39
48
|
|
40
49
|
cert_paths = extract_certificate_paths(options.verify_with)
|
data/lib/mdqt/client.rb
CHANGED
@@ -4,6 +4,7 @@ module MDQT
|
|
4
4
|
require 'rubygems'
|
5
5
|
require 'mdqt/client/metadata_service'
|
6
6
|
require 'mdqt/client/metadata_validator'
|
7
|
+
require 'mdqt/client/metadata_file'
|
7
8
|
require 'mdqt/client/identifier_utils'
|
8
9
|
|
9
10
|
begin
|
@@ -20,19 +21,22 @@ module MDQT
|
|
20
21
|
|
21
22
|
def initialize(base_url, options={})
|
22
23
|
|
23
|
-
@base_url
|
24
|
-
@verbose
|
25
|
-
@explain
|
24
|
+
@base_url = base_url
|
25
|
+
@verbose = options[:verbose] || false
|
26
|
+
@explain = options[:explain] || false
|
27
|
+
@tls_cert_check = options[:tls_risky] ? false : true
|
26
28
|
@cache_type = options[:cache_type] || :none
|
27
29
|
|
28
|
-
@md_service = MetadataService.new(@base_url, verbose: @verbose, cache_type: @cache_type, explain: @explain)
|
30
|
+
@md_service = MetadataService.new(@base_url, verbose: @verbose, cache_type: @cache_type, explain: @explain, tls_cert_check: tls_cert_check?)
|
29
31
|
|
30
32
|
end
|
31
33
|
|
32
|
-
def
|
34
|
+
def open_metadata(filename)
|
35
|
+
MetadataFile.new(filename, verbose: @verbose)
|
36
|
+
end
|
33
37
|
|
38
|
+
def get_metadata(entity_id)
|
34
39
|
md_service.get(entity_id)
|
35
|
-
|
36
40
|
end
|
37
41
|
|
38
42
|
def transform_uri(uri)
|
@@ -51,6 +55,10 @@ module MDQT
|
|
51
55
|
@explain
|
52
56
|
end
|
53
57
|
|
58
|
+
def tls_cert_check?
|
59
|
+
@tls_cert_check
|
60
|
+
end
|
61
|
+
|
54
62
|
def cache_type
|
55
63
|
@cache_type
|
56
64
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module MDQT
|
2
|
+
class Client
|
3
|
+
|
4
|
+
class MetadataFile
|
5
|
+
|
6
|
+
require 'digest'
|
7
|
+
|
8
|
+
def initialize(filename, options = {})
|
9
|
+
@filename = filename
|
10
|
+
@identifier = nil
|
11
|
+
@data = nil
|
12
|
+
@expires = nil
|
13
|
+
@etag = nil
|
14
|
+
@last_modified = nil
|
15
|
+
@explanation = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def filename
|
19
|
+
@filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def identifier
|
23
|
+
@identifier
|
24
|
+
end
|
25
|
+
|
26
|
+
def data
|
27
|
+
@data ||= File.read(filename)
|
28
|
+
end
|
29
|
+
|
30
|
+
def readable?
|
31
|
+
File.readable?(filename)
|
32
|
+
end
|
33
|
+
|
34
|
+
def type
|
35
|
+
@type
|
36
|
+
end
|
37
|
+
|
38
|
+
def expires
|
39
|
+
@expires
|
40
|
+
end
|
41
|
+
|
42
|
+
def etag
|
43
|
+
@etag
|
44
|
+
end
|
45
|
+
|
46
|
+
def last_modified
|
47
|
+
@last_modified
|
48
|
+
end
|
49
|
+
|
50
|
+
def ok?
|
51
|
+
@ok
|
52
|
+
end
|
53
|
+
|
54
|
+
def signed?
|
55
|
+
@data.include? "Signature" # This is... not great
|
56
|
+
end
|
57
|
+
|
58
|
+
def verified_signature?(certs = [], _ = {})
|
59
|
+
validator = MetadataValidator.new(certs: [certs].flatten)
|
60
|
+
validator.verified_signature?(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid?
|
64
|
+
validator = MetadataValidator.new
|
65
|
+
validator.valid?(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def validation_error
|
69
|
+
validator = MetadataValidator.new
|
70
|
+
validator.validation_error(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
def canonical_filename
|
74
|
+
if identifier.empty?
|
75
|
+
@filename = "aggregate-#{Digest::SHA1.hexdigest(@service)}.xml"
|
76
|
+
else
|
77
|
+
@filename ||= identifier.start_with?("{sha1}") ?
|
78
|
+
"#{@identifier.gsub("{sha1}","")}.xml" :
|
79
|
+
"#{Digest::SHA1.hexdigest(@identifier)}.xml"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -74,11 +74,21 @@ module MDQT
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def verified_signature?(certs = [], _ = {})
|
77
|
-
return true unless ok?
|
77
|
+
return true unless ok? # CHECK ?
|
78
78
|
validator = MetadataValidator.new(certs: [certs].flatten)
|
79
79
|
validator.verified_signature?(self)
|
80
80
|
end
|
81
81
|
|
82
|
+
def valid?
|
83
|
+
validator = MetadataValidator.new
|
84
|
+
validator.valid?(self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def validation_error
|
88
|
+
validator = MetadataValidator.new
|
89
|
+
validator.validation_error(self)
|
90
|
+
end
|
91
|
+
|
82
92
|
def filename
|
83
93
|
if identifier.empty?
|
84
94
|
@filename = "aggregate-#{Digest::SHA1.hexdigest(@service)}.xml"
|
@@ -123,6 +133,9 @@ module MDQT
|
|
123
133
|
@explanation
|
124
134
|
end
|
125
135
|
|
136
|
+
private
|
137
|
+
|
138
|
+
|
126
139
|
end
|
127
140
|
|
128
141
|
end
|
@@ -24,7 +24,7 @@ module MDQT
|
|
24
24
|
@store_config = options[:cache_store]
|
25
25
|
@verbose = options[:verbose] ? true : false
|
26
26
|
@explain = options[:explain] ? true : false
|
27
|
-
|
27
|
+
@tls_cert_check = options[:tls_cert_check] ? true : false
|
28
28
|
end
|
29
29
|
|
30
30
|
def base_url
|
@@ -42,7 +42,7 @@ module MDQT
|
|
42
42
|
req.options.open_timeout = 5
|
43
43
|
end
|
44
44
|
rescue Faraday::ConnectionFailed => oops
|
45
|
-
abort "Error - can't connect to MDQ service at URL #{base_url}"
|
45
|
+
abort "Error - can't connect to MDQ service at URL #{base_url}: #{oops.to_s}"
|
46
46
|
end
|
47
47
|
|
48
48
|
MetadataResponse.new(entity_id, base_url, http_response, explain: explain?)
|
@@ -70,6 +70,10 @@ module MDQT
|
|
70
70
|
@explain
|
71
71
|
end
|
72
72
|
|
73
|
+
def tls_cert_check?
|
74
|
+
@tls_cert_check
|
75
|
+
end
|
76
|
+
|
73
77
|
def cache?
|
74
78
|
cache_type == :none ? false : true
|
75
79
|
end
|
@@ -116,6 +120,7 @@ module MDQT
|
|
116
120
|
faraday.use FaradayMiddleware::Gzip
|
117
121
|
faraday.use FaradayMiddleware::FollowRedirects
|
118
122
|
faraday.use :http_cache, faraday_cache_config if cache?
|
123
|
+
faraday.ssl.verify = tls_cert_check?
|
119
124
|
faraday.headers['Accept'] = 'application/samlmetadata+xml'
|
120
125
|
faraday.headers['Accept-Charset'] = 'utf-8'
|
121
126
|
faraday.headers['User-Agent'] = "MDQT v#{MDQT::VERSION}"
|