omf_common 6.0.7.1 → 6.0.8.pre.1

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,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 65fd04c1481079b28cd3b353559c88893704fa25
4
- data.tar.gz: 98e1f8789175aaf85869f463b3add3201959a18e
5
- SHA512:
6
- metadata.gz: 477b8b778f5ddf8284d55cefff962dd3a3efec8cdfe705444ef69790d4e56cde08b6d8e7087339c7c2574c8056291520ee47e3e970b37bc756669f94435c0712
7
- data.tar.gz: 54d3f6a16aeff8e32a5eed85d6400af3ef9811a2db70bc562b3f4a7729376b1aabfe0c5481264629e920b331cc16b08d98fe7c4e3f7aeafeb5f1e76c7a91cdc9
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWJjMTBiYThjZmUxMWJjZDIyMmFiOTU2NDViZjQyMTM0MmQ4OTIwZA==
5
+ data.tar.gz: !binary |-
6
+ NTBjNDYwYzk3NzRlNjFhMTBlYjEzYTZmYjljZjUzZTM0NDVlOTc2ZA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MjJkNzIxN2NjZDMzOTcwZTVlNTg3YWZhY2I0NWEyYmI4MzEwMTRmNzg1ZDRh
10
+ M2RhY2RlZGZiNDkwZWI5MDAyMGExY2RiNTA3ZWUwMjg5ZmE2ODExYmIwOWVl
11
+ N2I2MDIzOTE0ZGE5NzBlODJjZTU0Yzc5YmFkMGI2MTY4NzM5NjE=
12
+ data.tar.gz: !binary |-
13
+ MzliNDQ5MTU3ZWJkZjg0Mzg5N2FiMmFhN2Y1ODlkZjFiYzY1YzQ3YzZiNGVi
14
+ MjlmNmZmNjEzNjc0M2RhYzZiZGU1NWM4NjY4YzQ3ZTMzMjg1MjE5OTYyNmQ4
15
+ YmNiYTU1NTI2ZTgzZTEwYzhjM2FkNjA2ZmU1NzRkMzM4OTYyZjY=
data/bin/omf_cert.rb ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env ruby
2
+ BIN_DIR = File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)
3
+ TOP_DIR = File.join(BIN_DIR, '..')
4
+ $: << File.join(TOP_DIR, 'lib')
5
+
6
+ require 'json/jwt' # to get around dependencies with activesupport
7
+
8
+ DESCR = %{
9
+ Program to create, read, and manipulate X509 certificates used in OMF.
10
+ create_root ...... Create a root certificate
11
+ create_user ...... Create user certificate (requires: --email, --user)
12
+ create_resource .. Create a certificate for a resource (requires: --resource_type)
13
+ describe ......... Print out some of the key properties found in the cert
14
+ }
15
+
16
+
17
+ require 'omf_common'
18
+ include OmfCommon::Auth
19
+
20
+ DEF_SUBJECT_PREFIX = Certificate.default_domain('US', 'CA', 'ACME', 'Roadrunner')
21
+
22
+ OPTS = {
23
+ duration: Certificate::DEF_DURATION
24
+ }
25
+
26
+ op = OP = OptionParser.new
27
+ op.banner = "\nUsage: #{op.program_name} [options] cmd \n#{DESCR}\n"
28
+ op.on '-o', '--out FILE', "Write result into FILE [STDOUT]" do |file|
29
+ OPTS[:out] = file
30
+ end
31
+ op.on '-i', '--in FILE', "Read certificate from FILE [STDIN]" do |file|
32
+ OPTS[:in] = file
33
+ end
34
+ op.on '--email EMAIL', "Email to add to cert" do |email|
35
+ OPTS[:email] = email
36
+ end
37
+ op.on '--cn CN', "Common name to use. Will be appended to '#{DEF_SUBJECT_PREFIX}'" do |cn|
38
+ OPTS[:cn] = cn
39
+ end
40
+ op.on '--subj SUBJECT', "Subject to use in cert [#{DEF_SUBJECT_PREFIX}/CN=dummy]" do |subject|
41
+ OPTS[:subject] = subject
42
+ end
43
+ op.on '--user USER_NAME', "User name for user certs" do |user|
44
+ OPTS[:user] = user
45
+ end
46
+ op.on '--resource-type TYPE', "Type of resource to create cert for" do |type|
47
+ OPTS[:resource_type] = type
48
+ end
49
+ op.on '--resource-id ID', "ID for resource" do |id|
50
+ OPTS[:resource_id] = id
51
+ end
52
+ op.on '--duration SEC', "Duration the cert will be valid for [#{OPTS[:duration]}]" do |secs|
53
+ OPTS[:duration] = secs
54
+ end
55
+ op.on '--domain C:ST:O:OU', "Domain to us (components are ':' separated) [#{DEF_SUBJECT_PREFIX}]" do |domain|
56
+ unless (p = domain.split(':')).length == 4
57
+ $stderr.puts "ERROR: Domain needs to contain 4 parts separated by ':'\n"
58
+ exit(-1)
59
+ end
60
+ c, st, o, ou = p
61
+ Certificate.default_domain(c, st, o, ou)
62
+ end
63
+
64
+ op.on_tail('-v', "--verbose", "Print summary of created cert (Surpressed when writing cert to stdout)") do
65
+ OPTS[:verbose] = true
66
+ end
67
+ op.on_tail('-h', "--help", "Show this message") { $stderr.puts op; exit }
68
+ rest = op.parse(ARGV) || []
69
+ OPTS[:verbose] = false unless OPTS[:out]
70
+
71
+ if rest.length != 1
72
+ $stderr.puts "ERROR: Can't figure out what is being requested\n"
73
+ $stderr.puts op; exit
74
+ end
75
+
76
+ CertificateStore.init()
77
+
78
+ def write(content)
79
+ if (fname = OPTS[:out]) && fname != '-'
80
+ File.open(fname, 'w') {|f| f.puts content}
81
+ else
82
+ puts content
83
+ end
84
+ end
85
+
86
+ def write_cert(cert)
87
+ write cert.to_pem_with_key
88
+ describe_cert(cert) if OPTS[:verbose]
89
+ end
90
+
91
+ def require_opts(*names)
92
+ fails = false
93
+ names.each do |n|
94
+ unless OPTS[n]
95
+ $stderr.puts "ERROR: Missing option '--#{n}'\n"
96
+ fails = true
97
+ end
98
+ end
99
+ exit if fails
100
+ end
101
+
102
+ def describe_cert(cert = nil)
103
+ unless cert
104
+ if cert_file = OPTS[:in]
105
+ if File.readable?(cert_file)
106
+ pem = File.read(cert_file)
107
+ else
108
+ $stderr.puts "ERROR: Can't open file '#{cert_file}' for reading\n"
109
+ exit
110
+ end
111
+ else
112
+ pem = $stdin.read
113
+ end
114
+ cert = Certificate.create_from_pem(pem)
115
+ end
116
+ cert.describe.each do |k, v|
117
+ puts "#{k}:#{' ' * (15 - k.length)} #{v.inspect}"
118
+ end
119
+ end
120
+
121
+ case cmd = rest[0]
122
+ when /^cre.*_root/
123
+ require_opts(:email)
124
+ cert = Certificate.create_root(OPTS)
125
+ write_cert cert
126
+
127
+ when /^cre.*_user/
128
+ root = Certificate.create_root()
129
+ require_opts(:user, :email)
130
+ cert = root.create_for_user(OPTS[:user], OPTS)
131
+ write_cert cert
132
+
133
+ when /^cre.*_resource/
134
+ root = Certificate.create_root()
135
+ require_opts(:resource_type)
136
+ r_id = OPTS.delete(:resource_id)
137
+ r_type = OPTS.delete(:resource_type)
138
+ cert = root.create_for_resource(r_id, r_type, OPTS)
139
+ write_cert cert
140
+
141
+ when /^des.*/ # describe
142
+ describe_cert
143
+ else
144
+ $stderr.puts "ERROR: Unknown cmd '#{cmd}'\n"
145
+ $stderr.puts op; exit
146
+ end
147
+
148
+ exit
149
+
150
+ # unless resource_url || resource_type
151
+ # $stderr.puts 'Missing --resource-url --type or'
152
+ # $stderr.puts op
153
+ # exit(-1)
154
+ # end
155
+
156
+ adam = root.create_for_user('adam')
157
+ projectA = root.create_for_resource('projectA', :project)
158
+ #puts projectA.to_pem
159
+
160
+ # require 'json/jwt'
161
+ # msg = {cnt: "shit", iss: projectA}
162
+ # p = JSON::JWT.new(msg).sign(projectA.key , :RS256).to_s
163
+
164
+ require 'omf_common/auth/jwt_authenticator'
165
+
166
+ #puts projectA.addresses_raw
167
+
168
+
169
+ p = OmfCommon::Auth::JWTAuthenticator.sign('shit', projectA)
170
+ pn = (p.length / 80 + 1).times.map {|i| p[i * 80, 80]}.join("\n")
171
+ puts pn
172
+
173
+ puts pn.split.join == p
174
+
175
+ puts OmfCommon::Auth::JWTAuthenticator.parse(pn).inspect
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This stand alone program is exercising a few authorization scenarios.
4
+ #
5
+ # Usage: ruby -I .
6
+
7
+ EX_DIR = File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)
8
+ TOP_DIR = File.join(EX_DIR, '..')
9
+ $: << File.join(TOP_DIR, 'lib')
10
+
11
+ begin; require 'json/jwt'; rescue Exception; end
12
+ require 'omf_common'
13
+ require 'pry'
14
+
15
+ OP_MODE = :development
16
+
17
+ opts = {
18
+ communication: {
19
+ url: 'xmpp://srv.mytestbed.net',
20
+ auth: {
21
+ authenticate: true,
22
+ pdp: {
23
+ constructor: 'TestPDP',
24
+ trust: ['adam']
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ # Implements a simple PDP which accepts any message from a set of trusted issuers.
31
+ #
32
+ class TestPDP
33
+
34
+ def initialize(opts = {})
35
+ @trust = opts[:trust] || []
36
+ puts "AUTH INIT>>> #{opts}"
37
+ end
38
+
39
+ def authorize(msg, &block)
40
+ info msg.to_s
41
+ iss = msg.issuer.resource_id
42
+ if @trust.include? iss
43
+ puts "AUTH(#{iss}) >>> PASS"
44
+ msg
45
+ else
46
+ puts "AUTH(#{iss}) >>> FAILED"
47
+ end
48
+ end
49
+ end
50
+
51
+ def doit(comm, el)
52
+ init_auth_store(comm)
53
+ comm.subscribe(:test) do |topic|
54
+ topic.on_message do |msg|
55
+ puts "MSG>> #{msg}"
56
+ end
57
+
58
+ topic.configure({foo: 1}, {issuer: 'adam'})
59
+ el.after(1) { topic.configure({foo: 2}, {issuer: 'eve'}) }
60
+ end
61
+ end
62
+
63
+ def init_auth_store(comm)
64
+ root_ca = OmfCommon::Auth::Certificate.create_root
65
+
66
+ root_ca.create_for_resource 'adam', :requester
67
+ root_ca.create_for_resource 'eve', :requester
68
+ OmfCommon::Auth::CertificateStore.instance.pry
69
+ end
70
+
71
+ OmfCommon.init(:development, opts) do |el|
72
+ OmfCommon.comm.on_connected do |comm|
73
+ doit(comm, el)
74
+ end
75
+ el.after(3) do puts "AFTER" end
76
+ end
data/lib/omf_common.rb CHANGED
@@ -142,6 +142,8 @@ module OmfCommon
142
142
  # @param [Hash] opts
143
143
  #
144
144
  def self.init(op_mode, opts = {}, &block)
145
+ opts = _rec_sym_keys(opts)
146
+
145
147
  if op_mode && defs = DEFAULTS[op_mode.to_sym]
146
148
  opts = _rec_merge(defs, opts)
147
149
  end
@@ -157,6 +159,7 @@ module OmfCommon
157
159
  if lopts = opts[:logging]
158
160
  _init_logging(lopts) unless lopts.empty?
159
161
  end
162
+
160
163
  unless copts = opts[:communication]
161
164
  raise "Missing :communication description"
162
165
  end
@@ -294,7 +297,7 @@ module OmfCommon
294
297
  end
295
298
  end
296
299
 
297
- # Recusively Symbolize keys of hash
300
+ # Recursively Symbolize keys of hash
298
301
  #
299
302
  def self._rec_sym_keys(hash)
300
303
  h = {}
@@ -325,4 +328,12 @@ module OmfCommon
325
328
  end
326
329
  end
327
330
  end
331
+
332
+ def self.load_credentials(opts)
333
+ unless opts.nil?
334
+ OmfCommon::Auth::CertificateStore.instance.register_default_certs(File.expand_path(opts[:root_cert_dir]))
335
+ cert_and_priv_key = File.read(File.expand_path(opts[:entity_cert])) << "\n" << File.read(File.expand_path(opts[:entity_key]))
336
+ OmfCommon::Auth::Certificate.create_from_pem(cert_and_priv_key)
337
+ end
338
+ end
328
339
  end
@@ -6,50 +6,141 @@
6
6
  require 'openssl'
7
7
  require 'omf_common/auth'
8
8
  require 'omf_common/auth/ssh_pub_key_convert'
9
+ require 'uuidtools'
9
10
 
10
11
  module OmfCommon::Auth
11
12
 
13
+ class CertificateNoLongerValidException < AuthException; end
14
+
12
15
  class Certificate
13
16
  DEF_DOMAIN_NAME = 'acme'
14
17
  DEF_DURATION = 3600
15
18
 
16
19
  BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n"
17
20
  END_CERT = "\n-----END CERTIFICATE-----\n"
18
- @@serial = 0
21
+ BEGIN_KEY = "-----BEGIN RSA PRIVATE KEY-----\n"
22
+ END_KEY = "\n-----END RSA PRIVATE KEY-----\n"
19
23
 
20
- # @param [String] name unique name of the entity (resource name)
21
- # @param [String] type type of the entity (resource type)
22
- # @param [String] domain of the resource
24
+ @@def_x509_name_prefix = [['C', 'US'], ['ST', 'CA'], ['O', 'ACME'], ['OU', 'Roadrunner']]
25
+ @@def_email_domain = 'acme.org'
23
26
  #
24
- def self.create(address, name, type, domain = DEF_DOMAIN_NAME, issuer = nil, not_before = Time.now, duration = 3600, key = nil)
25
- subject = _create_name(name, type, domain)
26
- if key.nil?
27
- key, digest = _create_key()
28
- else
27
+ def self.default_domain(country, state, organisation, org_unit)
28
+ @@def_x509_name_prefix = [
29
+ ['C', c = country.upcase],
30
+ ['ST', st = state.upcase],
31
+ ['O', o = organisation],
32
+ ['OU', ou = org_unit]
33
+ ]
34
+ "/C=#{c}/ST=#{st}/O=#{o}/OU=#{ou}"
35
+ end
36
+
37
+ def self.default_email_domain(email_domain)
38
+ @@def_email_domain = email_domain
39
+ end
40
+
41
+ # @param [String] resource_id unique id of the resource entity
42
+ # @param [String] resource_type type of the resource entity
43
+ # @param [Certificate] Issuer
44
+ # @param [Hash] options
45
+ # @option [Time] :not_before Time the cert will be valid from [now]
46
+ # @option [int] :duration Time in seconds this cert is valid for [3600]
47
+ # @option [OpenSSL::PKey::RSA] :key Key to encode in cert. If not given, will create a new one
48
+ # @option [String] :user_id ID (should be UUID) for user [UUID(email)]
49
+ # @option [String] :email Email to identify user. If not give, user 'name' and @@def_email_domain
50
+ # @option [String] :geni_uri
51
+ # @option [String] :frcp_uri
52
+ # @option [String] :frcp_domain
53
+ # @option [String] :http_uri
54
+ # @option [String] :http_prefix
55
+ #
56
+ def self.create_for_resource(resource_id, resource_type, issuer, opts = {})
57
+ xname = @@def_x509_name_prefix.dup
58
+ xname << ['CN', opts[:cn] || resource_id]
59
+ subject = OpenSSL::X509::Name.new(xname)
60
+
61
+ if key = opts[:key]
29
62
  digest = _create_digest
63
+ else
64
+ key, digest = _create_key()
30
65
  end
31
66
 
32
- c = _create_x509_cert(address, subject, key, digest, issuer, not_before, duration)
33
- c[:address] = address if address
67
+ addresses = opts[:addresses] || []
68
+ addresses << "URI:uuid:#{opts[:resource_uuid]}" if opts[:resource_uuid]
69
+ email_domain = opts[:email] ? opts[:email].split('@')[1] : @@def_email_domain
70
+ addresses << (opts[:geni_uri] || "URI:urn:publicid:IDN+#{email_domain}+#{resource_type}+#{resource_id}")
71
+ if frcp_uri = opts[:frcp_uri]
72
+ unless frcp_uri.to_s.start_with? 'URI'
73
+ frcp_uri = "URI:frcp:#{frcp_uri}"
74
+ end
75
+ addresses << frcp_uri
76
+ end
77
+ # opts[:frcp_uri] || "URI:frcp:#{user_id}@#{opts[:frcp_domain] || @@def_email_domain}",
78
+ # opts[:http_uri] || "URI:http://#{opts[:http_prefix] || @@def_email_domain}/users/#{user_id}"
79
+ not_before = opts[:not_before] || Time.now
80
+ duration = opts[:duration] = 3600
81
+ c = _create_x509_cert(subject, key, digest, issuer, not_before, duration, addresses)
82
+ c[:addresses] = addresses
83
+ c[:resource_id] = resource_id
84
+ c[:subject] = subject
34
85
  self.new c
35
86
  end
36
87
 
37
- # @param [String] pem is the content of existing x509 cert
38
- # @param [OpenSSL::PKey::RSA|String] key is the private key which can be attached to the instance for signing.
39
- def self.create_from_x509(pem, key = nil)
88
+ def self.create_root(opts = {})
89
+ email = opts[:email] ||= "sa@#{@@def_email_domain}"
90
+ opts = {
91
+ addresses: [
92
+ "email:#{email}"
93
+ ]
94
+ }.merge(opts)
95
+ cert = create_for_resource('sa', :authority, nil, opts)
96
+ CertificateStore.instance.register_trusted(cert)
97
+ cert
98
+ end
99
+
100
+ # Return a newly create certificate with properties token from
101
+ # 'pem' encoded string.
102
+ #
103
+ # @param [String] pem is the PEM encoded content of existing x509 cert
104
+ # @return [Certificate] Certificate object
105
+ #
106
+ def self.create_from_pem(pem_s)
107
+ state = :seeking
108
+ cert_pem = []
109
+ key_pem = []
110
+ end_regexp = /^-*END/
111
+ pem_s.each_line do |line|
112
+ state = :seeking if line.match(end_regexp)
113
+ case state
114
+ when :seeking
115
+ case line
116
+ when /^-*BEGIN CERTIFICATE/
117
+ state = :cert
118
+ when /^-*BEGIN RSA PRIVATE KEY/
119
+ state = :key
120
+ end
121
+ when :cert
122
+ cert_pem << line
123
+ when :key
124
+ key_pem << line
125
+ else
126
+ raise "BUG: Unknown state '#{state}'"
127
+ end
128
+ end
40
129
  # Some command list generated cert can use \r\n as newline char
41
- unless pem =~ /^-----BEGIN CERTIFICATE-----/
42
- pem = "#{BEGIN_CERT}#{pem}#{END_CERT}"
130
+ cert_pem = cert_pem.join()
131
+ unless cert_pem =~ /^-----BEGIN CERTIFICATE-----/
132
+ cert_pem = "#{BEGIN_CERT}#{cert_pem.chomp}#{END_CERT}"
43
133
  end
44
-
45
- cert = OpenSSL::X509::Certificate.new(pem)
46
-
47
- key = OpenSSL::PKey::RSA.new(key) if key && key.is_a?(String)
48
-
49
- if key && !cert.check_private_key(key)
50
- raise ArgumentError, "Private key provided could not match the public key of given certificate"
134
+ opts = {}
135
+ opts[:cert] = OpenSSL::X509::Certificate.new(cert_pem)
136
+ if key_pem.size > 0
137
+ key_pem = key_pem.join()
138
+ unless key_pem =~ /^-----BEGIN RSA PRIVATE KEY-----/
139
+ key_pem = "#{BEGIN_KEY}#{key_pem.chomp}#{END_KEY}"
140
+ end
141
+ opts[:key] = OpenSSL::PKey::RSA.new(key_pem)
51
142
  end
52
- self.new({ cert: cert, key: key })
143
+ self.new(opts)
53
144
  end
54
145
 
55
146
  # Returns an array with a new RSA key and a SHA1 digest
@@ -62,13 +153,6 @@ module OmfCommon::Auth
62
153
  OpenSSL::Digest::SHA1.new
63
154
  end
64
155
 
65
- # @param [String] name unique name of the entity (resource name)
66
- # @param [String] type type of the entity (resource type)
67
- #
68
- def self._create_name(name, type, domain = DEF_DOMAIN_NAME)
69
- OpenSSL::X509::Name.new [['CN', "frcp//#{domain}//frcp.#{type}.#{name}"]], {}
70
- end
71
-
72
156
  # Create a X509 certificate
73
157
  #
74
158
  # @param [String] address
@@ -76,96 +160,154 @@ module OmfCommon::Auth
76
160
  # @param [OpenSSL::PKey::RSA] key
77
161
  # @return {cert, key}
78
162
  #
79
- def self._create_x509_cert(address, subject, key, digest = nil,
80
- issuer = nil, not_before = Time.now, duration = DEF_DURATION, extensions = [])
81
- extensions << ["subjectAltName", "URI:#{address}", false] if address
163
+ def self._create_x509_cert(subject, key, digest = nil,
164
+ issuer = nil, not_before = Time.now, duration = DEF_DURATION, addresses = [])
165
+
166
+ if key.nil?
167
+ key, digest = _create_key()
168
+ else
169
+ digest = _create_digest
170
+ end
82
171
 
83
172
  cert = OpenSSL::X509::Certificate.new
84
173
  cert.version = 2
85
174
  # TODO change serial to non-sequential secure random numbers for production use
86
- cert.serial = (@@serial += 1)
175
+ cert.serial = UUIDTools::UUID.random_create.to_i
87
176
  cert.subject = subject
88
177
  cert.public_key = key.public_key
89
178
  cert.not_before = not_before
90
179
  cert.not_after = not_before + duration
91
- unless extensions.empty?
92
- issuer_cert = issuer ? issuer.to_x509 : cert
93
- ef = OpenSSL::X509::ExtensionFactory.new
94
- ef.subject_certificate = cert
95
- ef.issuer_certificate = issuer_cert
96
- extensions.each{|oid, value, critical|
97
- cert.add_extension(ef.create_extension(oid, value, critical))
98
- }
180
+ #extensions << ["subjectAltName", "URI:http://foo.com/users/dc766130, URI:frcp:dc766130-c822-11e0-901e-000c29f89f7b@foo.com", false]
181
+
182
+ issuer_cert = issuer ? issuer.to_x509 : cert
183
+ ef = OpenSSL::X509::ExtensionFactory.new
184
+ ef.subject_certificate = cert
185
+ ef.issuer_certificate = issuer_cert
186
+ unless addresses.empty?
187
+ cert.add_extension(ef.create_extension("subjectAltName", addresses.join(','), false))
99
188
  end
189
+
100
190
  if issuer
101
191
  cert.issuer = issuer.subject
102
192
  cert.sign(issuer.key, issuer.digest)
103
193
  else
104
194
  # self signed
105
195
  cert.issuer = subject
196
+
197
+ # Not exactly sure if that's the right extensions to add. Copied from
198
+ # http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/X509/Certificate.html
199
+ cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
200
+ cert.add_extension(ef.create_extension("keyUsage", "keyCertSign, cRLSign", true))
201
+ cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
202
+ cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
203
+
204
+ # Signing the cert should be ABSOLUTELY the last step
106
205
  cert.sign(key, digest)
107
206
  end
108
207
  { cert: cert, key: key }
109
208
  end
110
209
 
111
- attr_reader :address, :subject, :key, :digest
210
+ attr_reader :addresses, :resource_id # :addresses_raw, :addresses_string
211
+ attr_reader :subject, :key, :digest
212
+ attr_writer :resource_id
112
213
 
113
214
  def initialize(opts)
114
215
  if @cert = opts[:cert]
115
216
  @subject = @cert.subject
116
217
  end
117
- unless @address = opts[:address]
118
- # try to see it it is in cert
119
- if @cert
120
- @cert.extensions.each do |ext|
121
- if ext.oid == 'subjectAltName'
122
- @address = ext.value[4 .. -1] # strip off 'URI:'
123
- end
124
- end
125
- end
126
- end
127
- if @key = opts[:key]
128
- @digest = opts[:digest] || OpenSSL::Digest::SHA1.new
129
- end
218
+ @resource_id = opts[:resource_id]
219
+ _extract_addresses(@cert)
130
220
  unless @subject ||= opts[:subject]
131
221
  name = opts[:name]
132
222
  type = opts[:type]
133
223
  domain = opts[:domain]
134
224
  @subject = _create_name(name, type, domain)
135
225
  end
136
- @cert ||= _create_x509_cert(@address, @subject, @key, @digest)[:cert]
226
+ if key = opts[:key]
227
+ @digest = opts[:digest] || self.class._create_digest
228
+ end
229
+ if @cert
230
+ self.key = key if key # this verifies that key is the right one for this cert
231
+ else
232
+ #@cert ||= _create_x509_cert(@address, @subject, @key, @digest)[:cert]
233
+ @cert = self.class._create_x509_cert(@subject, key, @digest)[:cert]
234
+ @key = key
235
+ end
137
236
  end
138
237
 
139
- # @param [OpenSSL::PKey::RSA|String] key is most likely the public key of the resource.
140
- #
141
- def create_for(address, name, type, domain = DEF_DOMAIN_NAME, duration = 3600, key = nil)
142
- raise ArgumentError, "Address required" unless address
238
+ def valid?
239
+ now = Time.new
240
+ (@cert.not_before <= now && now <= @cert.not_after)
241
+ end
242
+
243
+ def cert_expired?
244
+ debug "Certificate expired!" unless valid?
245
+ !valid?
246
+ end
143
247
 
144
- begin
145
- key = OpenSSL::PKey::RSA.new(key) if key && key.is_a?(String)
146
- rescue OpenSSL::PKey::RSAError
147
- # It might be a SSH pub key, try that
148
- key = OmfCommon::Auth::SSHPubKeyConvert.convert(key)
248
+ def key=(key)
249
+ if @cert && !@cert.check_private_key(key)
250
+ raise ArgumentError, "Private key provided could not match the public key of given certificate"
149
251
  end
252
+ @key = key
253
+ end
150
254
 
151
- cert = self.class.create(address, name, type, domain, self, Time.now, duration, key)
152
- CertificateStore.instance.register(cert, address)
255
+ def create_for_resource(resource_id, resource_type, opts = {})
256
+ unless valid?
257
+ raise CertificateNoLongerValidException.new
258
+ end
259
+ resource_id ||= UUIDTools::UUID.random_create()
260
+ unless opts[:resource_uuid]
261
+ if resource_id.is_a? UUIDTools::UUID
262
+ opts[:resource_uuid] = resource_id
263
+ else
264
+ opts[:resource_uuid] = UUIDTools::UUID.random_create()
265
+ end
266
+ end
267
+ unless opts[:cn]
268
+ opts[:cn] = "#{resource_id}/type=#{resource_type}"
269
+ (opts[:cn] += "/uuid=#{opts[:resource_uuid]}") unless resource_id.is_a? UUIDTools::UUID
270
+ end
271
+ cert = self.class.create_for_resource(resource_id, resource_type, self, opts)
272
+ CertificateStore.instance.register(cert)
153
273
  cert
154
274
  end
155
275
 
276
+ # See #create_for_resource for documentation on 'opts'
277
+ def create_for_user(name, opts = {})
278
+ unless valid?
279
+ raise CertificateNoLongerValidException.new
280
+ end
281
+ email = opts[:email] || "#{name}@#{@@def_email_domain}"
282
+ user_id = opts[:user_id] || UUIDTools::UUID.sha1_create(UUIDTools::UUID_URL_NAMESPACE, email)
283
+ opts[:cn] = "#{user_id}/emailAddress=#{email}"
284
+ opts[:addresses] = [
285
+ "email:#{email}",
286
+ ]
287
+ create_for_resource(user_id, :user, opts)
288
+ end
289
+
156
290
  # Return the X509 certificate. If it hasn't been passed in, return a self-signed one
157
291
  def to_x509()
158
292
  @cert
159
293
  end
160
294
 
161
295
  def can_sign?
162
- !@key.nil? && @key.private?
296
+ !cert_expired? && !@key.nil? && @key.private?
297
+ end
298
+
299
+ def root_ca?
300
+ subject == @cert.issuer
163
301
  end
164
302
 
165
303
  def to_pem
166
304
  to_x509.to_pem
167
305
  end
168
306
 
307
+ def to_pem_with_key
308
+ to_x509.to_pem + @key.to_pem
309
+ end
310
+
169
311
  def to_pem_compact
170
312
  to_pem.lines.to_a[1 ... -1].join.strip
171
313
  end
@@ -178,16 +320,57 @@ module OmfCommon::Auth
178
320
  end
179
321
  end
180
322
 
323
+ # Return a hash of some of the key properties of this cert.
324
+ # To get the full monty, use 'openssl x509 -in xxx.pem -text'
325
+ #
326
+ def describe
327
+ {
328
+ subject: subject,
329
+ issuer: @cert.issuer,
330
+ addresses: addresses,
331
+ can_sign: can_sign?,
332
+ root_ca: root_ca?,
333
+ valid: valid?,
334
+ valid_period: [@cert.not_before, @cert.not_after]
335
+ }
336
+ #(@cert.methods - Object.new.methods).sort
337
+ end
338
+
181
339
  # Will return one of the following
182
340
  #
183
341
  # :HS256, :HS384, :HS512, :RS256, :RS384, :RS512, :ES256, :ES384, :ES512
184
342
  #
185
343
  # def key_algorithm
186
- #
344
+ #
187
345
  # end
188
346
 
189
347
  def to_s
190
- "#<#{self.class} addr=#{@address} subj=#{@subject} can-sign=#{@key != nil}>"
348
+ "#<#{self.class} subj=#{@subject} can-sign=#{@key != nil}>"
349
+ end
350
+
351
+ def _extract_addresses(cert)
352
+ addr = @addresses = {}
353
+ return unless cert
354
+ ext = cert.extensions.find { |ext| ext.oid == 'subjectAltName' }
355
+ return unless ext
356
+ @address_string = ext.value
357
+ @addresses_raw = ext.value.split(',').compact
358
+ @addresses_raw.each do |addr_s|
359
+ parts = addr_s.split(':')
360
+ #puts ">>>>>> #{parts}"
361
+ case parts[0].strip
362
+ when 'email'
363
+ addr[:email] = parts[1]
364
+ when 'URI'
365
+ if parts[1] == 'urn'
366
+ addr[:geni] = parts[3][4 .. -1]
367
+ else
368
+ addr[parts[1].to_sym] = parts[2]
369
+ end
370
+ else
371
+ warn "Unknown address type '#{parts[0]}'"
372
+ end
373
+ end
191
374
  end
192
375
  end # class
193
376
  end # module