omf_common 6.0.7.1 → 6.0.8.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|