mdqt 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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}"
|