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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 410463d3ecf907b09d8f051c40ade36cfe5b65a7d5fa593f85b4a6f6901ceb0f
4
- data.tar.gz: 247dfc548b20015479a6b309940bf04053c09f9d889211a7f78517d2cb2c63d4
3
+ metadata.gz: 7fbccd299cf9c1a2432b72d55bd74c8c35633529a42137b4ebf82c020a3407d2
4
+ data.tar.gz: 954092c1deb38ba21374b19bbd00bc2412e92508e95eeda2b7ebb6ada9916ba4
5
5
  SHA512:
6
- metadata.gz: bdc5aec58160d10cf264d290d6bff4d200124a1bcbb52b377a027d3c1c30e896c7b7ef881d9f972a20935abea94c9f310206eb74ad828b598d42e863627f7708
7
- data.tar.gz: e476961884bffe5b9be0438293eba6292246be51a2afc78412f550741cdd97e3eb4a14ca17967bbcac44a1fc0d23f2f96facd080f0d394a4bdaef5868f33ee6c
6
+ metadata.gz: 064fc97f1e597b7d14febeb892f3176e142bcba1b224bf8e46453d1ead108becb2915788e285adf7f5089c4755a28d1d4a4a4ad92a1e580b2b0e2083042deb89
7
+ data.tar.gz: 476527f791f3d62efae084ad84632131657383bb7697c6a575f77b12ee6054a1fcbd3c91ec29cd1f304a36f32c67e6522a73281da9d6095f8f9dab2a901b60cb
data/.gitignore CHANGED
@@ -15,3 +15,4 @@
15
15
  /mdq-beta-cert.pem
16
16
  /out
17
17
  /xout
18
+ out*.xml
@@ -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 just need
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 just use the `transform` command:
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
 
@@ -9,6 +9,7 @@ module MDQT
9
9
  require 'mdqt/cli/reset'
10
10
  require 'mdqt/cli/version'
11
11
  require 'mdqt/cli/transform'
12
+ require 'mdqt/cli/check'
12
13
 
13
14
 
14
15
  end
@@ -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 is not available. Install the 'xmldsig' gem if you can." unless MDQT::Client.verification_available?
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
+
@@ -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)
@@ -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 = base_url
24
- @verbose = options[:verbose] || false
25
- @explain = options[:explain] || false
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 get_metadata(entity_id)
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}"