~awstools-dev/ubuntu/maverick/ec2-ami-tools/maverick

« back to all changes in this revision

Viewing changes to lib/ec2/amitools/crypto.rb

  • Committer: Bazaar Package Importer
  • Author(s): Chuck Short
  • Date: 2008-10-14 08:35:25 UTC
  • Revision ID: james.westby@ubuntu.com-20081014083525-c0n69wr7r7aqfb8w
Tags: 1.3-26357-0ubuntu2
* New upstream version.
* Update the debian copyright file.
* Added quilt patch system to make it easier to maintain. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2008 Amazon.com, Inc. or its affiliates.  All Rights
 
2
# Reserved.  Licensed under the Amazon Software License (the
 
3
# "License").  You may not use this file except in compliance with the
 
4
# License. A copy of the License is located at
 
5
# http://aws.amazon.com/asl or in the "license" file accompanying this
 
6
# file.  This file is distributed on an "AS IS" BASIS, WITHOUT
 
7
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
 
8
# the License for the specific language governing permissions and
 
9
# limitations under the License.
 
10
 
 
11
require 'ec2/amitools/format'
 
12
require 'digest/sha1'
 
13
require 'openssl'
 
14
require 'stringio'
 
15
 
 
16
### 
 
17
# Cryptographic utilities module.
 
18
#
 
19
module Crypto
 
20
  BUFFER_SIZE = 1024 * 1024
 
21
  ASYM_ALG = 'RSA'
 
22
  SYM_ALG = 'AES-128-CBC'
 
23
  DIGEST_ALG = 'SHA1'
 
24
  PADDING = OpenSSL::PKey::RSA::PKCS1_PADDING
 
25
  VERSION1 = 1
 
26
  VERSION2 = 2
 
27
  SHA1_FINGERPRINT_REGEX = /([a-f0-9]{2}(:[a-f0-9]{2}){15})/i
 
28
 
 
29
  #----------------------------------------------------------------------------#
 
30
 
 
31
  ##
 
32
  # Decrypt the specified cipher text according to the AMI Manifest Encryption
 
33
  # Scheme Version 1 or 2.
 
34
  #
 
35
  # ((|cipher_text|)) The cipher text to decrypt.
 
36
  # ((|keyio_or_keyfilename|)) The key data IO stream or the name of the private
 
37
  # key file.
 
38
  #
 
39
  def Crypto.decryptasym(cipher_text, keyfilename)
 
40
    raise ArgumentError.new('cipher_text') unless cipher_text
 
41
    raise ArgumentError.new('keyfilename') unless keyfilename and FileTest.exists? keyfilename
 
42
 
 
43
    # Load key.
 
44
    privkey = File.open(keyfilename, 'r') { |f| OpenSSL::PKey::RSA.new(f) }
 
45
 
 
46
    # Get version.
 
47
    version = cipher_text[0]
 
48
    if version == VERSION2
 
49
      return Crypto.decryptasym_v2( cipher_text, keyfilename )
 
50
    end
 
51
    raise ArgumentError.new("invalid encryption scheme versionb: #{version}") unless version == 1
 
52
    
 
53
    # Decrypt and extract encrypted symmetric key and initialization vector.
 
54
    symkey_cryptogram_len = cipher_text.slice(1, 2).unpack('C')[0]
 
55
    symkey_cryptogram = privkey.private_decrypt(
 
56
      cipher_text.slice(2, symkey_cryptogram_len),
 
57
      PADDING)
 
58
    symkey = symkey_cryptogram.slice(0, 16)
 
59
    iv = symkey_cryptogram.slice(16, 16)
 
60
    
 
61
    # Decrypt data with the symmetric key.
 
62
    cryptogram = cipher_text.slice(2 + symkey_cryptogram_len..cipher_text.size)
 
63
    decryptsym(cryptogram, symkey, iv)
 
64
  end
 
65
 
 
66
  #----------------------------------------------------------------------------#
 
67
 
 
68
  ##
 
69
  # Decrypt the specified cipher text according to the AMI Manifest Encryption
 
70
  # Scheme Version 2.
 
71
  #
 
72
  # ((|cipher_text|)) The cipher text to decrypt.
 
73
  # ((|keyio_or_keyfilename|)) The key data IO stream or the name of the private
 
74
  # key file.
 
75
  #
 
76
  def Crypto.decryptasym_v2(cipher_text, keyfilename)
 
77
    raise ArgumentError.new('cipher_text') unless cipher_text
 
78
    raise ArgumentError.new('keyfilename') unless keyfilename and FileTest.exists? keyfilename
 
79
 
 
80
    # Load key.
 
81
    privkey = File.open(keyfilename, 'r') { |f| OpenSSL::PKey::RSA.new(f) }
 
82
 
 
83
    # Get version.
 
84
    version = cipher_text[0]
 
85
    raise ArgumentError.new("invalid encryption scheme versionb: #{version}") unless version == VERSION2
 
86
    
 
87
    # Decrypt and extract encrypted symmetric key and initialization vector.
 
88
    hi_byte, lo_byte = cipher_text.slice(1, 3).unpack('CC')
 
89
    symkey_cryptogram_len = ( hi_byte << 8 ) | lo_byte        
 
90
    symkey_cryptogram = privkey.private_decrypt(
 
91
      cipher_text.slice(3, symkey_cryptogram_len),
 
92
      PADDING)
 
93
    
 
94
    symkey = symkey_cryptogram.slice(0, 16)
 
95
    iv = symkey_cryptogram.slice(16, 16)
 
96
        
 
97
    # Decrypt data with the symmetric key.
 
98
    cryptogram = cipher_text.slice( ( 3 + symkey_cryptogram_len )..cipher_text.size)
 
99
    decryptsym(cryptogram, symkey, iv)
 
100
  end
 
101
 
 
102
  #----------------------------------------------------------------------------#
 
103
 
 
104
  ##
 
105
  # Asymmetrically encrypt the specified data using the AMI Manifest Encryption
 
106
  # Scheme Version 2.
 
107
  #
 
108
  # The data is encrypted with an ephemeral symmetric key and initialization
 
109
  # vector. The symmetric key and initialization vector are encrypted with the
 
110
  # specified public key and preprended to the data.
 
111
  #
 
112
  # ((|data|)) The data to encrypt.
 
113
  # ((|pubkey|)) The public key.
 
114
  #
 
115
  def Crypto.encryptasym(data, pubkey)
 
116
    raise ArgumentError.new('data') unless data
 
117
    raise ArgumentError.new('pubkey') unless pubkey
 
118
    
 
119
    symkey = gensymkey
 
120
    iv = geniv
 
121
    symkey_cryptogram = pubkey.public_encrypt( symkey + iv, PADDING )
 
122
 
 
123
    data_cryptogram = encryptsym(data, symkey, iv)
 
124
        
 
125
    hi_byte, lo_byte = Format.int2int16(symkey_cryptogram.size)
 
126
    
 
127
    Format::int2byte(VERSION2) + hi_byte + lo_byte + symkey_cryptogram + data_cryptogram
 
128
  end
 
129
 
 
130
  #----------------------------------------------------------------------------#
 
131
  
 
132
  ##
 
133
  # Verify the authenticity of the data from the IO stream or string ((|data|))
 
134
  # using the signature ((|sig|)) and the public key ((|pubkey|)).
 
135
  #
 
136
  # Return true iff the signature is valid.
 
137
  #
 
138
  def Crypto.authenticate(data, sig, pubkey)
 
139
    raise ArgumentError.new("Invalid parameter data") if data.nil?
 
140
    raise ArgumentError.new("Invalid parameter sig") if sig.nil? or sig.length==0
 
141
    raise ArgumentError.new("Invalid parameter pubkey") if pubkey.nil?
 
142
         
 
143
    # Create IO stream if necessary.
 
144
    io = (data.instance_of?(StringIO) ? data : StringIO.new(data))
 
145
    
 
146
    sha = OpenSSL::Digest::SHA1.new
 
147
    res = false
 
148
    while not (io.eof?)
 
149
      res = pubkey.verify(sha, sig, io.read(BUFFER_SIZE))
 
150
    end
 
151
    res
 
152
  end
 
153
 
 
154
  #----------------------------------------------------------------------------#
 
155
  
 
156
  ##
 
157
  # Decrypt the specified cipher text file to create the specified plain text
 
158
  # file.
 
159
  #
 
160
  # The symmetric cipher is AES in CBC mode. 128 bit keys are used. If the plain
 
161
  # text file already exists it will be overwritten.
 
162
  #
 
163
  # ((|src|)) The name of the cipher text file to decrypt.
 
164
  # ((|dst|)) The name of the plain text file to create.
 
165
  # ((|key|)) The 128 bit (16 byte) symmetric key.
 
166
  # ((|iv|)) The 128 bit (16 byte) initialization vector.
 
167
  #
 
168
  def Crypto.decryptfile(src, dst, key, iv)    
 
169
    raise ArgumentError.new("invalid file name: #{src}") unless FileTest.exists?(src)
 
170
    raise ArgumentError.new("invalid key") unless key and key.size == 16
 
171
    raise ArgumentError.new("invalid iv") unless iv and iv.size == 16
 
172
    pio = IO.popen( "openssl enc -d -aes-128-cbc -in #{src} -out #{dst} -K #{Format::bin2hex(key)} -iv #{Format::bin2hex(iv)} 2>&1" )
 
173
    result = pio.read
 
174
    pio.close
 
175
    raise "error decrypting file #{src}: #{result}" if result.strip != ''
 
176
  end
 
177
 
 
178
  #----------------------------------------------------------------------------#
 
179
 
 
180
  ##
 
181
  # Decrypt _ciphertext_ using _key_ and _iv_ using AES-128-CBC.
 
182
  #
 
183
  def Crypto.decryptsym(plaintext, key, iv)
 
184
    raise ArgumentError.new("plaintext must be a String") unless plaintext.is_a? String
 
185
    raise ArgumentError.new("invalid key") unless key.is_a? String and key.size == 16
 
186
    raise ArgumentError.new("invalid iv") unless iv.is_a? String and iv.size == 16
 
187
    
 
188
    cipher = OpenSSL::Cipher::Cipher.new( 'AES-128-CBC' )
 
189
    cipher.decrypt( key, iv )
 
190
    # NOTE: If the key and iv aren't set this doesn't work correctly.
 
191
    cipher.key = key
 
192
    cipher.iv = iv
 
193
    plaintext = cipher.update( plaintext )
 
194
    plaintext + cipher.final
 
195
  end
 
196
 
 
197
  #----------------------------------------------------------------------------#
 
198
 
 
199
  ##
 
200
  # Generate and return a message digest for the data from the IO stream
 
201
  # ((|io|)), using the algorithm alg
 
202
  #
 
203
  def Crypto.digest(io, alg = OpenSSL::Digest::SHA1.new)
 
204
    raise ArgumentError.new('io') unless io.kind_of?(IO) or io.kind_of?(StringIO)
 
205
    while not io.eof?
 
206
      alg.update(io.read(BUFFER_SIZE))
 
207
    end
 
208
    alg.digest
 
209
  end
 
210
  
 
211
  #----------------------------------------------------------------------------#
 
212
  
 
213
  # Return the HMAC SHA1 of _data_ using _key_.
 
214
  def Crypto.hmac_sha1( key, data )
 
215
    raise ParameterError.new( "key must be a String" ) unless key.is_a? String
 
216
    raise ParameterError.new( "data must be a String" ) unless data.is_a? String
 
217
    
 
218
    md = OpenSSL::Digest::SHA1.new
 
219
    hmac = OpenSSL::HMAC.new( key, md)
 
220
    hmac.update( data )
 
221
    return hmac.digest
 
222
  end
 
223
  
 
224
  #----------------------------------------------------------------------------#
 
225
  
 
226
  ##
 
227
  # Decrypt the specified cipher text file to create the specified plain text
 
228
  # file.
 
229
  #
 
230
  # The symmetric cipher is AES in CBC mode. 128 bit keys are used. If the plain
 
231
  # text file already exists it will be overwritten.
 
232
  #
 
233
  # ((|key|)) The 128 bit (16 byte) symmetric key.
 
234
  # ((|src|)) The name of the cipher text file to encrypt.
 
235
  # ((|dst|)) The name of the plain text file to create.
 
236
  # ((|iv|)) The 128 bit (16 byte) initialization vector.
 
237
  #
 
238
  def Crypto.encryptfile(src, dst, key, iv)
 
239
    raise ArgumentError.new("invalid file name: #{src}") unless FileTest.exists?(src)
 
240
    raise ArgumentError.new("invalid key") unless key and key.size == 16
 
241
    raise ArgumentError.new("invalid iv") unless iv and iv.size == 16
 
242
    cmd = "openssl enc -e -aes-128-cbc -in #{src} -out #{dst} -K #{Format::bin2hex(key)} -iv #{Format::bin2hex(iv)}"
 
243
    result = Kernel::system(cmd)
 
244
    raise "error encrypting file #{src}" unless result
 
245
  end
 
246
 
 
247
  #----------------------------------------------------------------------------#
 
248
 
 
249
  ##
 
250
  # Encrypt _plaintext_ with _key_ and _iv_ using AES-128-CBC.
 
251
  #
 
252
  def Crypto.encryptsym(plaintext, key, iv)
 
253
    raise ArgumentError.new("plaintext must be a String") unless plaintext.kind_of? String
 
254
    raise ArgumentError.new("invalid key") unless ( key.is_a? String and key.size == 16 )
 
255
    raise ArgumentError.new("invalid iv") unless ( iv.is_a? String and iv.size == 16 )
 
256
    
 
257
    cipher = OpenSSL::Cipher::Cipher.new( 'AES-128-CBC' )
 
258
    cipher.encrypt( key, iv )
 
259
    # NOTE: If the key and iv aren't set this doesn't work correctly.
 
260
    cipher.key = key
 
261
    cipher.iv = iv
 
262
    ciphertext = cipher.update( plaintext )
 
263
    ciphertext + cipher.final
 
264
  end
 
265
 
 
266
  #----------------------------------------------------------------------------#
 
267
 
 
268
  ##
 
269
  # Generate an initialization vector suitable use with symmetric cipher.
 
270
  #
 
271
  def Crypto.geniv
 
272
    OpenSSL::Cipher::Cipher.new(SYM_ALG).random_iv
 
273
  end
 
274
 
 
275
  #----------------------------------------------------------------------------#
 
276
 
 
277
  ##  
 
278
  # Generate a key suitable for use with a symmetric cipher.
 
279
  #
 
280
  def Crypto.gensymkey
 
281
    OpenSSL::Cipher::Cipher.new(SYM_ALG).random_key
 
282
  end
 
283
 
 
284
  #----------------------------------------------------------------------------#
 
285
 
 
286
  ##
 
287
  # Return the public key from the X509 certificate file ((|filename|)). 
 
288
  #
 
289
  def Crypto.certfile2pubkey(filename)
 
290
    begin
 
291
      File.open(filename) do |f|
 
292
        return cert2pubkey(f)
 
293
      end
 
294
    rescue Exception => e
 
295
      raise "error reading certificate file #{filename}: #{e.message}"
 
296
    end
 
297
  end
 
298
  
 
299
  #----------------------------------------------------------------------------#
 
300
  
 
301
  def Crypto.cert2pubkey(data)
 
302
    begin
 
303
      return OpenSSL::X509::Certificate.new(data).public_key
 
304
    rescue Exception => e
 
305
      raise "error reading certificate: #{e.message}"
 
306
    end
 
307
  end
 
308
  
 
309
  #----------------------------------------------------------------------------#
 
310
 
 
311
  ##
 
312
  # Sign the data from IO stream or string ((|data|)) using the key in
 
313
  # ((|keyfilename|)).
 
314
  #
 
315
  # Return the signature.
 
316
  #
 
317
  def Crypto.sign(data, keyfilename)
 
318
    raise ArgumentError.new('data') unless data
 
319
    raise ArgumentError.new("invalid file name: #{keyfilename}") unless FileTest.exists?(keyfilename)
 
320
 
 
321
    # Create an IO stream from the data if necessary.
 
322
    io = (data.instance_of?(StringIO) ? data : StringIO.new(data))
 
323
 
 
324
    sha = OpenSSL::Digest::SHA1.new
 
325
    pk  = loadprivkey( keyfilename )
 
326
    return pk.sign(sha, io.read )
 
327
  end
 
328
 
 
329
  #------------------------------------------------------------------------------#
 
330
 
 
331
  ##
 
332
  # Generate the SHA1 fingerprint for a PEM-encoded certificate (NOT private key)
 
333
  # Returns the fingerprint in aa:bb:... form
 
334
  # Raises ArgumentError if the fingerprint cannot be obtained
 
335
  #
 
336
  def Crypto.cert_sha1_fingerprint(cert_filename)
 
337
    raise ArgumentError.new('cert_filename is nil')  if cert_filename.nil?
 
338
    raise ArgumentError.new("invalid cert file name: #{cert_filename}") unless FileTest.exists?(cert_filename)
 
339
    fingerprint = nil
 
340
    
 
341
    IO.popen("openssl x509 -in #{cert_filename} -noout -sha1 -fingerprint") do |io|
 
342
      out = io.read
 
343
      md = SHA1_FINGERPRINT_REGEX.match(out)
 
344
      if md
 
345
        fingerprint = md[1]
 
346
      end
 
347
    end
 
348
  
 
349
    raise ArgumentError.new("could not generate fingerprint for #{cert_filename}")  if fingerprint.nil?
 
350
    
 
351
    return fingerprint    
 
352
  end
 
353
  
 
354
  #------------------------------------------------------------------------------#
 
355
  
 
356
  def Crypto.loadprivkey filename
 
357
    begin
 
358
      OpenSSL::PKey::RSA.new( File.open( filename,'r' ) )
 
359
    rescue Exception => e
 
360
      raise "error reading private key from file #{filename}: #{e.message}"
 
361
    end
 
362
  end
 
363
  
 
364
  #----------------------------------------------------------------------------#
 
365
  
 
366
  ##
 
367
  # XOR the byte string ((|a|)) with the byte string ((|b|)). The operans must
 
368
  # be of the same length.
 
369
  #
 
370
  def Crypto.xor(a, b)
 
371
    raise ArgumentError.new('data lengths differ') unless a.size == b.size
 
372
    xored = String.new
 
373
    a.size.times do |i|
 
374
      xored << (a[i] ^ b[i])
 
375
    end
 
376
    
 
377
    xored
 
378
  end
 
379
end