rightscale-nanite 0.4.1 → 0.4.1.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.
- data/lib/nanite.rb +71 -0
- data/lib/nanite/actor.rb +60 -0
- data/lib/nanite/actor_registry.rb +24 -0
- data/lib/nanite/admin.rb +153 -0
- data/lib/nanite/agent.rb +250 -0
- data/lib/nanite/amqp.rb +47 -0
- data/lib/nanite/cluster.rb +203 -0
- data/lib/nanite/config.rb +102 -0
- data/lib/nanite/console.rb +39 -0
- data/lib/nanite/daemonize.rb +13 -0
- data/lib/nanite/dispatcher.rb +90 -0
- data/lib/nanite/identity.rb +16 -0
- data/lib/nanite/job.rb +104 -0
- data/lib/nanite/local_state.rb +34 -0
- data/lib/nanite/log.rb +64 -0
- data/lib/nanite/log/formatter.rb +39 -0
- data/lib/nanite/mapper.rb +277 -0
- data/lib/nanite/mapper_proxy.rb +56 -0
- data/lib/nanite/packets.rb +231 -0
- data/lib/nanite/pid_file.rb +52 -0
- data/lib/nanite/reaper.rb +38 -0
- data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
- data/lib/nanite/security/certificate.rb +55 -0
- data/lib/nanite/security/certificate_cache.rb +66 -0
- data/lib/nanite/security/distinguished_name.rb +34 -0
- data/lib/nanite/security/encrypted_document.rb +46 -0
- data/lib/nanite/security/rsa_key_pair.rb +53 -0
- data/lib/nanite/security/secure_serializer.rb +67 -0
- data/lib/nanite/security/signature.rb +40 -0
- data/lib/nanite/security/static_certificate_store.rb +35 -0
- data/lib/nanite/security_provider.rb +47 -0
- data/lib/nanite/serializer.rb +52 -0
- data/lib/nanite/state.rb +164 -0
- data/lib/nanite/streaming.rb +125 -0
- data/lib/nanite/util.rb +51 -0
- data/spec/actor_registry_spec.rb +62 -0
- data/spec/actor_spec.rb +59 -0
- data/spec/agent_spec.rb +235 -0
- data/spec/cached_certificate_store_proxy_spec.rb +34 -0
- data/spec/certificate_cache_spec.rb +49 -0
- data/spec/certificate_spec.rb +27 -0
- data/spec/cluster_spec.rb +300 -0
- data/spec/dispatcher_spec.rb +136 -0
- data/spec/distinguished_name_spec.rb +24 -0
- data/spec/encrypted_document_spec.rb +21 -0
- data/spec/job_spec.rb +219 -0
- data/spec/local_state_spec.rb +112 -0
- data/spec/packet_spec.rb +218 -0
- data/spec/rsa_key_pair_spec.rb +33 -0
- data/spec/secure_serializer_spec.rb +41 -0
- data/spec/serializer_spec.rb +107 -0
- data/spec/signature_spec.rb +30 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/static_certificate_store_spec.rb +30 -0
- data/spec/util_spec.rb +63 -0
- metadata +62 -1
@@ -0,0 +1,66 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Implements a simple LRU cache: items that are the least accessed are
|
4
|
+
# deleted first.
|
5
|
+
class CertificateCache
|
6
|
+
|
7
|
+
# Max number of items to keep in memory
|
8
|
+
DEFAULT_CACHE_MAX_COUNT = 100
|
9
|
+
|
10
|
+
# Initialize cache
|
11
|
+
def initialize(max_count = DEFAULT_CACHE_MAX_COUNT)
|
12
|
+
@items = {}
|
13
|
+
@list = []
|
14
|
+
@max_count = max_count
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add item to cache
|
18
|
+
def put(key, item)
|
19
|
+
if @items.include?(key)
|
20
|
+
delete(key)
|
21
|
+
end
|
22
|
+
if @list.size == @max_count
|
23
|
+
delete(@list.first)
|
24
|
+
end
|
25
|
+
@items[key] = item
|
26
|
+
@list.push(key)
|
27
|
+
item
|
28
|
+
end
|
29
|
+
alias :[]= :put
|
30
|
+
|
31
|
+
# Retrieve item from cache
|
32
|
+
# Store item returned by given block if any
|
33
|
+
def get(key)
|
34
|
+
if @items.include?(key)
|
35
|
+
@list.each_index do |i|
|
36
|
+
if @list[i] == key
|
37
|
+
@list.delete_at(i)
|
38
|
+
break
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@list.push(key)
|
42
|
+
@items[key]
|
43
|
+
else
|
44
|
+
return nil unless block_given?
|
45
|
+
self[key] = yield
|
46
|
+
end
|
47
|
+
end
|
48
|
+
alias :[] :get
|
49
|
+
|
50
|
+
# Delete item from cache
|
51
|
+
def delete(key)
|
52
|
+
c = @items[key]
|
53
|
+
if c
|
54
|
+
@items.delete(key)
|
55
|
+
@list.each_index do |i|
|
56
|
+
if @list[i] == key
|
57
|
+
@list.delete_at(i)
|
58
|
+
break
|
59
|
+
end
|
60
|
+
end
|
61
|
+
c
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Build X.509 compliant distinguished names
|
4
|
+
# Distinghuished names are used to desccribe both a certificate issuer and
|
5
|
+
# subject.
|
6
|
+
class DistinguishedName
|
7
|
+
|
8
|
+
# Initialize distinguished name from hash
|
9
|
+
# e.g.:
|
10
|
+
# { 'C' => 'US',
|
11
|
+
# 'ST' => 'California',
|
12
|
+
# 'L' => 'Santa Barbara',
|
13
|
+
# 'O' => 'RightScale',
|
14
|
+
# 'OU' => 'Certification Services',
|
15
|
+
# 'CN' => 'rightscale.com/emailAddress=cert@rightscale.com' }
|
16
|
+
#
|
17
|
+
def initialize(hash)
|
18
|
+
@value = hash
|
19
|
+
end
|
20
|
+
|
21
|
+
# Conversion to OpenSSL X509 DN
|
22
|
+
def to_x509
|
23
|
+
if @value
|
24
|
+
OpenSSL::X509::Name.new(@value.to_a, OpenSSL::X509::Name::OBJECT_TYPE_TEMPLATE)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Human readable form
|
29
|
+
def to_s
|
30
|
+
'/' + @value.to_a.collect { |p| p.join('=') }.join('/') if @value
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Represents a signed an encrypted document that can be later decrypted using
|
4
|
+
# the right private key and whose signature can be verified using the right
|
5
|
+
# cert.
|
6
|
+
# This class can be used both to encrypt and sign data and to then check the
|
7
|
+
# signature and decrypt an encrypted document.
|
8
|
+
class EncryptedDocument
|
9
|
+
|
10
|
+
# Encrypt and sign data using certificate and key pair.
|
11
|
+
#
|
12
|
+
# Arguments:
|
13
|
+
# - 'data': Data to be encrypted
|
14
|
+
# - 'certs': Recipient certificates (certificates corresponding to private
|
15
|
+
# keys that may be used to decrypt data)
|
16
|
+
# - 'cipher': Cipher used for encryption, AES 256 CBC by default
|
17
|
+
#
|
18
|
+
def initialize(data, certs, cipher = 'AES-256-CBC')
|
19
|
+
cipher = OpenSSL::Cipher::Cipher.new(cipher)
|
20
|
+
certs = [ certs ] unless certs.respond_to?(:collect)
|
21
|
+
raw_certs = certs.collect { |c| c.raw_cert }
|
22
|
+
@pkcs7 = OpenSSL::PKCS7.encrypt(raw_certs, data, cipher, OpenSSL::PKCS7::BINARY)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initialize from encrypted data.
|
26
|
+
def self.from_data(encrypted_data)
|
27
|
+
doc = EncryptedDocument.allocate
|
28
|
+
doc.instance_variable_set(:@pkcs7, OpenSSL::PKCS7::PKCS7.new(encrypted_data))
|
29
|
+
doc
|
30
|
+
end
|
31
|
+
|
32
|
+
# Encrypted data using DER format
|
33
|
+
def encrypted_data
|
34
|
+
@pkcs7.to_pem
|
35
|
+
end
|
36
|
+
|
37
|
+
# Decrypted data
|
38
|
+
#
|
39
|
+
# Arguments:
|
40
|
+
# - 'key': Key used for decryption
|
41
|
+
# - 'cert': Certificate to use for decryption
|
42
|
+
def decrypted_data(key, cert)
|
43
|
+
@pkcs7.decrypt(key.raw_key, cert.raw_cert)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Allows generating RSA key pairs and extracting public key component
|
4
|
+
# Note: Creating a RSA key pair can take a fair amount of time (seconds)
|
5
|
+
class RsaKeyPair
|
6
|
+
|
7
|
+
DEFAULT_LENGTH = 2048
|
8
|
+
|
9
|
+
# Underlying OpenSSL keys
|
10
|
+
attr_reader :raw_key
|
11
|
+
|
12
|
+
# Create new RSA key pair using 'length' bits
|
13
|
+
def initialize(length = DEFAULT_LENGTH)
|
14
|
+
@raw_key = OpenSSL::PKey::RSA.generate(length)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Does key pair include private key?
|
18
|
+
def has_private?
|
19
|
+
raw_key.private?
|
20
|
+
end
|
21
|
+
|
22
|
+
# New RsaKeyPair instance with identical public key but no private key
|
23
|
+
def to_public
|
24
|
+
RsaKeyPair.from_data(raw_key.public_key.to_pem)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Key material in PEM format
|
28
|
+
def data
|
29
|
+
raw_key.to_pem
|
30
|
+
end
|
31
|
+
alias :to_s :data
|
32
|
+
|
33
|
+
# Load key pair previously serialized via 'data'
|
34
|
+
def self.from_data(data)
|
35
|
+
res = RsaKeyPair.allocate
|
36
|
+
res.instance_variable_set(:@raw_key, OpenSSL::PKey::RSA.new(data))
|
37
|
+
res
|
38
|
+
end
|
39
|
+
|
40
|
+
# Load key from file
|
41
|
+
def self.load(file)
|
42
|
+
from_data(File.read(file))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Save key to file in PEM format
|
46
|
+
def save(file)
|
47
|
+
File.open(file, "w") do |f|
|
48
|
+
f.write(@raw_key.to_pem)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Serializer implementation which secures messages by using
|
4
|
+
# X.509 certificate sigining.
|
5
|
+
class SecureSerializer
|
6
|
+
|
7
|
+
# Initialize serializer, must be called prior to using it.
|
8
|
+
#
|
9
|
+
# - 'identity': Identity associated with serialized messages
|
10
|
+
# - 'cert': Certificate used to sign and decrypt serialized messages
|
11
|
+
# - 'key': Private key corresponding to 'cert'
|
12
|
+
# - 'store': Certificate store. Exposes certificates used for
|
13
|
+
# encryption and signature validation.
|
14
|
+
# - 'encrypt': Whether data should be signed and encrypted ('true')
|
15
|
+
# or just signed ('false'), 'true' by default.
|
16
|
+
#
|
17
|
+
def self.init(identity, cert, key, store, encrypt = true)
|
18
|
+
@identity = identity
|
19
|
+
@cert = cert
|
20
|
+
@key = key
|
21
|
+
@store = store
|
22
|
+
@encrypt = encrypt
|
23
|
+
end
|
24
|
+
|
25
|
+
# Was serializer initialized?
|
26
|
+
def self.initialized?
|
27
|
+
@identity && @cert && @key && @store
|
28
|
+
end
|
29
|
+
|
30
|
+
# Serialize message and sign it using X.509 certificate
|
31
|
+
def self.dump(obj)
|
32
|
+
raise "Missing certificate identity" unless @identity
|
33
|
+
raise "Missing certificate" unless @cert
|
34
|
+
raise "Missing certificate key" unless @key
|
35
|
+
raise "Missing certificate store" unless @store || !@encrypt
|
36
|
+
json = obj.to_json
|
37
|
+
if @encrypt
|
38
|
+
certs = @store.get_recipients(obj)
|
39
|
+
json = EncryptedDocument.new(json, certs).encrypted_data if certs
|
40
|
+
end
|
41
|
+
sig = Signature.new(json, @cert, @key)
|
42
|
+
{ 'id' => @identity, 'data' => json, 'signature' => sig.data, 'encrypted' => !certs.nil? }.to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
# Unserialize data using certificate store
|
46
|
+
def self.load(json)
|
47
|
+
begin
|
48
|
+
raise "Missing certificate store" unless @store
|
49
|
+
raise "Missing certificate" unless @cert || !@encrypt
|
50
|
+
raise "Missing certificate key" unless @key || !@encrypt
|
51
|
+
data = JSON.load(json)
|
52
|
+
sig = Signature.from_data(data['signature'])
|
53
|
+
certs = @store.get_signer(data['id'])
|
54
|
+
certs = [ certs ] unless certs.respond_to?(:each)
|
55
|
+
jsn = data['data'] if certs.any? { |c| sig.match?(c) }
|
56
|
+
if jsn && @encrypt && data['encrypted']
|
57
|
+
jsn = EncryptedDocument.from_data(jsn).decrypted_data(@key, @cert)
|
58
|
+
end
|
59
|
+
JSON.load(jsn) if jsn
|
60
|
+
rescue Exception => e
|
61
|
+
Nanite::Log.error("Loading of secure packet failed: #{e.message}\n#{e.backtrace.join("\n")}")
|
62
|
+
raise
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Signature that can be validated against certificates
|
4
|
+
class Signature
|
5
|
+
|
6
|
+
FLAGS = OpenSSL::PKCS7::NOCERTS || OpenSSL::PKCS7::BINARY || OpenSSL::PKCS7::NOATTR || OpenSSL::PKCS7::NOSMIMECAP || OpenSSL::PKCS7::DETACH
|
7
|
+
|
8
|
+
# Create signature using certificate and key pair.
|
9
|
+
#
|
10
|
+
# Arguments:
|
11
|
+
# - 'data': Data to be signed
|
12
|
+
# - 'cert': Certificate used for signature
|
13
|
+
# - 'key': RsaKeyPair used for signature
|
14
|
+
#
|
15
|
+
def initialize(data, cert, key)
|
16
|
+
@p7 = OpenSSL::PKCS7.sign(cert.raw_cert, key.raw_key, data, [], FLAGS)
|
17
|
+
@store = OpenSSL::X509::Store.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Load signature previously serialized via 'data'
|
21
|
+
def self.from_data(data)
|
22
|
+
sig = Signature.allocate
|
23
|
+
sig.instance_variable_set(:@p7, OpenSSL::PKCS7::PKCS7.new(data))
|
24
|
+
sig.instance_variable_set(:@store, OpenSSL::X509::Store.new)
|
25
|
+
sig
|
26
|
+
end
|
27
|
+
|
28
|
+
# 'true' if signature was created using given cert, 'false' otherwise
|
29
|
+
def match?(cert)
|
30
|
+
@p7.verify([cert.raw_cert], @store, nil, OpenSSL::PKCS7::NOVERIFY)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Signature in PEM format
|
34
|
+
def data
|
35
|
+
@p7.to_pem
|
36
|
+
end
|
37
|
+
alias :to_s :data
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Simple certificate store, serves a static set of certificates.
|
4
|
+
class StaticCertificateStore
|
5
|
+
|
6
|
+
# Initialize store:
|
7
|
+
#
|
8
|
+
# - Signer certificates are used when loading data to check the digital
|
9
|
+
# signature. The signature associated with the serialized data needs
|
10
|
+
# to match with one of the signer certificates for loading to succeed.
|
11
|
+
#
|
12
|
+
# - Recipient certificates are used when serializing data for encryption.
|
13
|
+
# Loading the data can only be done through serializers that have been
|
14
|
+
# initialized with a certificate that's in the recipient certificates if
|
15
|
+
# encryption is enabled.
|
16
|
+
#
|
17
|
+
def initialize(signer_certs, recipients_certs)
|
18
|
+
signer_certs = [ signer_certs ] unless signer_certs.respond_to?(:each)
|
19
|
+
@signer_certs = signer_certs
|
20
|
+
recipients_certs = [ recipients_certs ] unless recipients_certs.respond_to?(:each)
|
21
|
+
@recipients_certs = recipients_certs
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve signer certificate for given id
|
25
|
+
def get_signer(identity)
|
26
|
+
@signer_certs
|
27
|
+
end
|
28
|
+
|
29
|
+
# Recipient certificate(s) that will be able to decrypt the serialized data
|
30
|
+
def get_recipients(obj)
|
31
|
+
@recipients_certs
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Nanite
|
2
|
+
# This class is used to interface the nanite mapper with an external security
|
3
|
+
# module.
|
4
|
+
# There are two points of integration:
|
5
|
+
# 1. When an agent registers with a mapper
|
6
|
+
# 2. When an agent sends a request to another agent
|
7
|
+
#
|
8
|
+
# In both these cases the security module is called back and can deny the
|
9
|
+
# operation.
|
10
|
+
# Note: it's the responsability of the module to do any logging or
|
11
|
+
# notification that is required.
|
12
|
+
class SecurityProvider
|
13
|
+
|
14
|
+
# Register an external security module
|
15
|
+
# This module should expose the 'authorize_registration' and
|
16
|
+
# 'authorize_request' methods.
|
17
|
+
def self.register(mod)
|
18
|
+
@security_module = mod
|
19
|
+
end
|
20
|
+
|
21
|
+
# Used internally by nanite to retrieve the current security module
|
22
|
+
def self.get
|
23
|
+
@security_module || default_security_module
|
24
|
+
end
|
25
|
+
|
26
|
+
# Default security module, authorizes all operations
|
27
|
+
def self.default_security_module
|
28
|
+
@default_sec_mod ||= DefaultSecurityModule.new
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Default security module
|
34
|
+
class DefaultSecurityModule
|
35
|
+
|
36
|
+
# Authorize registration of agent (registration is an instance of Register)
|
37
|
+
def authorize_registration(registration)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Authorize given inter-agent request (request is an instance of Request)
|
42
|
+
def authorize_request(request)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Nanite
|
2
|
+
class Serializer
|
3
|
+
|
4
|
+
class SerializationError < StandardError
|
5
|
+
attr_accessor :action, :packet
|
6
|
+
def initialize(action, packet, serializers, msg = nil)
|
7
|
+
@action, @packet = action, packet
|
8
|
+
msg = ":\n#{msg}" if msg && !msg.empty?
|
9
|
+
super("Could not #{action} #{packet.inspect} using #{serializers.inspect}#{msg}")
|
10
|
+
end
|
11
|
+
end # SerializationError
|
12
|
+
|
13
|
+
# The secure serializer should not be part of the cascading
|
14
|
+
def initialize(preferred_format = :marshal)
|
15
|
+
preferred_format ||= :marshal
|
16
|
+
if preferred_format.to_s == 'secure'
|
17
|
+
@serializers = [ SecureSerializer ]
|
18
|
+
else
|
19
|
+
preferred_serializer = SERIALIZERS[preferred_format.to_sym]
|
20
|
+
@serializers = SERIALIZERS.values.clone
|
21
|
+
@serializers.unshift(@serializers.delete(preferred_serializer)) if preferred_serializer
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def dump(packet)
|
26
|
+
cascade_serializers(:dump, packet)
|
27
|
+
end
|
28
|
+
|
29
|
+
def load(packet)
|
30
|
+
cascade_serializers(:load, packet)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
SERIALIZERS = {:json => JSON, :marshal => Marshal, :yaml => YAML}.freeze
|
36
|
+
|
37
|
+
def cascade_serializers(action, packet)
|
38
|
+
errors = []
|
39
|
+
@serializers.map do |serializer|
|
40
|
+
begin
|
41
|
+
o = serializer.send(action, packet)
|
42
|
+
rescue Exception => e
|
43
|
+
o = nil
|
44
|
+
errors << "#{e.message}\n\t#{e.backtrace[0]}"
|
45
|
+
end
|
46
|
+
return o if o
|
47
|
+
end
|
48
|
+
raise SerializationError.new(action, packet, @serializers, errors.join("\n"))
|
49
|
+
end
|
50
|
+
|
51
|
+
end # Serializer
|
52
|
+
end # Nanite
|