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 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}"