Class: Equity::Controller::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/equity/controller/key.rb

Constant Summary collapse

LENGTH =

Length, in bits, of generated keys.

2048
DEFAULT_IDENTITY_PATH =

Default file system path where the user’s key is stored. This path is the private half of the key. The public key can be found by appending “.pub” to this path.

File.join(ENV['HOME'], ".equity", "id_dsa")
PERSONAL_AUTHORIZED_KEYS_PATH =

Array of paths that contain the lists of public keys that identify people who are authorized to control equity.

File.join(ENV['HOME'], ".equity", "authorized_keys")
SYSTEM_AUTHORIZED_KEYS_PATH =
"/etc/equity/authorized_keys"
SIGNATURE_SEPARATOR =

Byte sequence that separates the data and the signature in signed data.

0.chr

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path_or_pem = DEFAULT_IDENTITY_PATH) ⇒ Key

Loads a key from disk or a string. If the loaded key includes the private half of the key and the key has been loaded from disk, the file’s permissions are verified to ensure that only the owner can access it. Raises a SecurityError if the file’s permissions are unsafe.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/equity/controller/key.rb', line 48

def initialize(path_or_pem = DEFAULT_IDENTITY_PATH)
  # Try to interpret path_or_pem as a PEM string first.
  begin
    @dsa = OpenSSL::PKey::DSA.new(path_or_pem)
  rescue
    # That didn't work. It must be a path.
    @path = path_or_pem
    @dsa = OpenSSL::PKey::DSA.new(IO.read(@path))
    # If this is a private key, make sure nobody else can read it.
    if @dsa.private?
      mode = File.stat(@path).mode
      unless mode & 077 == 0
        raise SecurityError, "unsafe permissions on #{@path} - must not allow any access by group or world"
      end
    end
  end
end

Class Method Details

.authorized_keysObject

Returns an array of keys which are authorized to control equity. Raises a SecurityError if either of the authorized key files are writable by group or world, or if a private key is found in the files.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/equity/controller/key.rb', line 126

def self.authorized_keys
  # Return cached keys if they've already been loaded.
  return @authorized_keys if @authorized_keys
  # Read the keys and verify file permissions.
  pem = ""
  if File.exist?(PERSONAL_AUTHORIZED_KEYS_PATH)
    mode = File.stat(PERSONAL_AUTHORIZED_KEYS_PATH).mode
    unless mode & 022 == 0
      raise SecurityError, "unsafe permissions on #{PERSONAL_AUTHORIZED_KEYS_PATH} - must not be writable by group or world"
    end
    pem = IO.read(PERSONAL_AUTHORIZED_KEYS_PATH) + "\n"
  end
  if File.exist?(SYSTEM_AUTHORIZED_KEYS_PATH)
    mode = File.stat(SYSTEM_AUTHORIZED_KEYS_PATH).mode
    unless mode & 022 == 0
      raise SecurityError, "unsafe permissions on #{SYSTEM_AUTHORIZED_KEYS_PATH} - must not be writable by group or world"
    end
    pem += IO.read(SYSTEM_AUTHORIZED_KEYS_PATH)
  end
  # Instantiate the keys and verify that they're all public.
  pems = pem.split(/-----END DSA PUBLIC KEY-----/)
  pems.pop
  pems.collect! do |pem|
    key = new(pem + "-----END DSA PUBLIC KEY-----")
    if key.private?
      raise SecurityError, "private key found in authorized keys - only public keys should be authorized"
    end
    key
  end
  # Cache the loaded keys.
  @authorized_keys = pems
end

.generateObject

Generates a new DSA key pair and stores it in a pair of files at the default identity path. Returns the generated key.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/equity/controller/key.rb', line 27

def self.generate
  # Generate a DSA key pair.
  dsa = OpenSSL::PKey::DSA.generate(LENGTH)
  # Write the private key to disk.
  FileUtils.mkdir_p(File.dirname(DEFAULT_IDENTITY_PATH))
  File.open(DEFAULT_IDENTITY_PATH, "w") do |io|
    io.chmod(0600)
    io.write(dsa.to_pem)
  end
  # Write the public key to disk.
  File.open(DEFAULT_IDENTITY_PATH + ".pub", "w") do |io|
    io.write(dsa.public_key.to_pem)
  end
  # Return a Key object with the newly generated key.
  new
end

.signature_key(data_with_sig) ⇒ Object

Returns the public half of the key that signed data_with_sig.



170
171
172
173
# File 'lib/equity/controller/key.rb', line 170

def self.signature_key(data_with_sig)
  data, sig, public_pem = data_with_sig.split(/#{SIGNATURE_SEPARATOR}/)
  new(public_pem)
end

.verify(data_with_sig) ⇒ Object

Verifies the cryptographic signature appended to data_with_sig. If the signature is legitimate, the data is returned. Otherwise this method returns false.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/equity/controller/key.rb', line 101

def self.verify(data_with_sig)
  data, sig, public_pem = data_with_sig.split(/#{SIGNATURE_SEPARATOR}/)
  sig = Base64.decode64(sig)
  dsa = OpenSSL::PKey::DSA.new(public_pem)
  sig_r = sig[0,20].unpack("H*")[0].to_i(16)
  sig_s = sig[20,20].unpack("H*")[0].to_i(16)
  a1sig = OpenSSL::ASN1::Sequence([
     OpenSSL::ASN1::Integer(sig_r),
     OpenSSL::ASN1::Integer(sig_s)
  ])
  if dsa.verify(OpenSSL::Digest::DSS1.new, a1sig.to_der, data)
    data
  else
    false
  end
end

Instance Method Details

#==(other) ⇒ Object

Two keys are equal if they have the same public key.



165
166
167
# File 'lib/equity/controller/key.rb', line 165

def ==(other)
  @dsa.public_key.to_s == other.instance_eval {@dsa.public_key.to_s}
end

#authorized?Boolean

Returns true if this key is authorized to control equity.

Returns:

  • (Boolean)


119
120
121
# File 'lib/equity/controller/key.rb', line 119

def authorized?
  self.class.authorized_keys.include?(self)
end

#pathsObject

Returns an array of paths to all files associated with this key.



67
68
69
70
71
72
73
74
# File 'lib/equity/controller/key.rb', line 67

def paths
  return [] if @path.nil?
  if @dsa.private?
    [@path, @path + ".pub"]
  else
    [@path + ".pub"]
  end
end

#private?Boolean

Returns true if this key includes the private half of the key.

Returns:

  • (Boolean)


160
161
162
# File 'lib/equity/controller/key.rb', line 160

def private?
  @dsa.private?
end

#sign(data) ⇒ Object

Appends a cryptographic signature to data.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/equity/controller/key.rb', line 77

def sign(data)
  sig = @dsa.sign(OpenSSL::Digest::DSS1.new, data)
  a1sig = OpenSSL::ASN1.decode( sig )

  sig_r = a1sig.value[0].value.to_s(2)
  sig_s = a1sig.value[1].value.to_s(2)

  if sig_r.length > 20 || sig_s.length > 20
    raise OpenSSL::PKey::DSAError, "bad sig size"
  end

  sig_r = "\0" * ( 20 - sig_r.length ) + sig_r if sig_r.length < 20
  sig_s = "\0" * ( 20 - sig_s.length ) + sig_s if sig_s.length < 20

  return [
    data,
    Base64.encode64(sig_r + sig_s),
    @dsa.public_key.to_pem
  ].join(SIGNATURE_SEPARATOR)
end