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.
Files changed (56) hide show
  1. data/lib/nanite.rb +71 -0
  2. data/lib/nanite/actor.rb +60 -0
  3. data/lib/nanite/actor_registry.rb +24 -0
  4. data/lib/nanite/admin.rb +153 -0
  5. data/lib/nanite/agent.rb +250 -0
  6. data/lib/nanite/amqp.rb +47 -0
  7. data/lib/nanite/cluster.rb +203 -0
  8. data/lib/nanite/config.rb +102 -0
  9. data/lib/nanite/console.rb +39 -0
  10. data/lib/nanite/daemonize.rb +13 -0
  11. data/lib/nanite/dispatcher.rb +90 -0
  12. data/lib/nanite/identity.rb +16 -0
  13. data/lib/nanite/job.rb +104 -0
  14. data/lib/nanite/local_state.rb +34 -0
  15. data/lib/nanite/log.rb +64 -0
  16. data/lib/nanite/log/formatter.rb +39 -0
  17. data/lib/nanite/mapper.rb +277 -0
  18. data/lib/nanite/mapper_proxy.rb +56 -0
  19. data/lib/nanite/packets.rb +231 -0
  20. data/lib/nanite/pid_file.rb +52 -0
  21. data/lib/nanite/reaper.rb +38 -0
  22. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  23. data/lib/nanite/security/certificate.rb +55 -0
  24. data/lib/nanite/security/certificate_cache.rb +66 -0
  25. data/lib/nanite/security/distinguished_name.rb +34 -0
  26. data/lib/nanite/security/encrypted_document.rb +46 -0
  27. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  28. data/lib/nanite/security/secure_serializer.rb +67 -0
  29. data/lib/nanite/security/signature.rb +40 -0
  30. data/lib/nanite/security/static_certificate_store.rb +35 -0
  31. data/lib/nanite/security_provider.rb +47 -0
  32. data/lib/nanite/serializer.rb +52 -0
  33. data/lib/nanite/state.rb +164 -0
  34. data/lib/nanite/streaming.rb +125 -0
  35. data/lib/nanite/util.rb +51 -0
  36. data/spec/actor_registry_spec.rb +62 -0
  37. data/spec/actor_spec.rb +59 -0
  38. data/spec/agent_spec.rb +235 -0
  39. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  40. data/spec/certificate_cache_spec.rb +49 -0
  41. data/spec/certificate_spec.rb +27 -0
  42. data/spec/cluster_spec.rb +300 -0
  43. data/spec/dispatcher_spec.rb +136 -0
  44. data/spec/distinguished_name_spec.rb +24 -0
  45. data/spec/encrypted_document_spec.rb +21 -0
  46. data/spec/job_spec.rb +219 -0
  47. data/spec/local_state_spec.rb +112 -0
  48. data/spec/packet_spec.rb +218 -0
  49. data/spec/rsa_key_pair_spec.rb +33 -0
  50. data/spec/secure_serializer_spec.rb +41 -0
  51. data/spec/serializer_spec.rb +107 -0
  52. data/spec/signature_spec.rb +30 -0
  53. data/spec/spec_helper.rb +23 -0
  54. data/spec/static_certificate_store_spec.rb +30 -0
  55. data/spec/util_spec.rb +63 -0
  56. 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