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 +14 -6
- data/bin/omf_cert.rb +175 -0
- data/example/auth_test.rb +76 -0
- data/lib/omf_common.rb +12 -1
- data/lib/omf_common/auth/certificate.rb +255 -72
- data/lib/omf_common/auth/certificate_store.rb +39 -15
- data/lib/omf_common/auth/jwt_authenticator.rb +69 -0
- data/lib/omf_common/auth/pdp/test_pdp.rb +21 -0
- data/lib/omf_common/comm.rb +8 -1
- data/lib/omf_common/comm/amqp/amqp_communicator.rb +7 -1
- data/lib/omf_common/comm/amqp/amqp_mp.rb +29 -0
- data/lib/omf_common/comm/amqp/amqp_topic.rb +4 -2
- data/lib/omf_common/comm/local/local_topic.rb +14 -14
- data/lib/omf_common/comm/xmpp/communicator.rb +14 -6
- data/lib/omf_common/comm/xmpp/xmpp_mp.rb +2 -2
- data/lib/omf_common/message.rb +27 -6
- data/lib/omf_common/message/json/json_message.rb +36 -21
- data/lib/omf_common/message/xml/message.rb +27 -21
- data/lib/omf_common/version.rb +8 -1
- data/omf_common.gemspec +4 -3
- data/test/fixture/alice-cert.pem +26 -0
- data/test/fixture/alice-key.pem +15 -0
- data/test/omf_common/auth/certificate_spec.rb +20 -41
- data/test/omf_common/auth/certificate_store_spec.rb +19 -21
- data/test/omf_common/comm/xmpp/communicator_spec.rb +4 -1
- data/test/omf_common/message/xml/message_spec.rb +2 -2
- metadata +66 -39
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
#
|
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
|
-
|
21
|
+
BEGIN_KEY = "-----BEGIN RSA PRIVATE KEY-----\n"
|
22
|
+
END_KEY = "\n-----END RSA PRIVATE KEY-----\n"
|
19
23
|
|
20
|
-
|
21
|
-
|
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.
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
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(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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(
|
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(
|
80
|
-
issuer = nil, not_before = Time.now, duration = DEF_DURATION,
|
81
|
-
|
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 =
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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 :
|
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
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
152
|
-
|
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}
|
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
|