cyberplat_pki 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # CyberplatPKI
2
2
 
3
- CyberplatPKI is an FFI binding for signing Cyberplat requests. It includes both Linux and Windows versions of Cyberplat-provided library as well as the necessary wrapping code.
3
+ CyberplatPKI 1.0 is an FFI binding for signing Cyberplat requests. It includes both Linux and Windows versions of Cyberplat-provided library as well as the necessary wrapping code. Note that this version only works on 32-bit Linux and Windows.
4
+
5
+ CyberplatPKI 2.0 is a pure-Ruby reimplementation of the reverse-engineered signing algorithm. It should be completely compatible with the vendor-provided library. This version works everywhere.
6
+
7
+ Select the variant you'd like to install with the version specification. `gem 'cyberplat_pki', '~> 1.0'` requests the FFI version, and `gem 'cyberplat_pki', '~> 2.0'` requests the pure-Ruby one.
4
8
 
5
9
  ## Installation
6
10
 
@@ -4,10 +4,10 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "cyberplat_pki"
7
- gem.version = "1.0.0"
7
+ gem.version = "2.0.0"
8
8
  gem.authors = ["Peter Zotov"]
9
9
  gem.email = ["whitequark@whitequark.org"]
10
- gem.description = %q{CyberplatPKI is an FFI binding for signing Cyberplat requests.}
10
+ gem.description = %q{CyberplatPKI is a library for signing Cyberplat requests.}
11
11
  gem.summary = gem.description
12
12
  gem.homepage = "http://github.com/whitequark/cyberplat_pki"
13
13
 
@@ -16,6 +16,9 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
18
 
19
- gem.add_dependency "ffi"
20
19
  gem.add_development_dependency "rspec"
20
+ gem.add_dependency 'digest-crc' # For CRC24
21
+ gem.add_dependency 'crypt' # For IDEA
22
+ gem.add_dependency "jruby-openssl" if RUBY_PLATFORM == "java"
23
+ gem.add_dependency "openssl" if RUBY_PLATFORM == "ruby"
21
24
  end
@@ -0,0 +1,82 @@
1
+ require "stringio"
2
+ require "base64"
3
+ require "digest/crc24"
4
+
5
+ module CyberplatPKI
6
+ class Document
7
+ attr_accessor :engine, :type, :subject, :ca, :body, :signature, :data_length
8
+
9
+ def initialize
10
+ @engine = nil
11
+ @type = nil
12
+ @subject = nil
13
+ @ca = nil
14
+ @body = nil
15
+ @signature = nil
16
+ @unknown1 = nil
17
+ end
18
+
19
+ def self.load(source)
20
+ source = source.sub /^[ \t\n\r]*/, ''
21
+
22
+ io = StringIO.new source, "rb"
23
+ io.extend DocumentIORoutines
24
+
25
+ documents = []
26
+
27
+ until io.eof?
28
+ begin
29
+ documents << io.read_document
30
+ rescue EOFError => e
31
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (unexpected end of document)"
32
+ end
33
+ end
34
+
35
+ documents
36
+ end
37
+
38
+ def self.save(documents)
39
+ io = StringIO.new '', "wb"
40
+ io.extend DocumentIORoutines
41
+
42
+ documents.each { |document| io.write_document document }
43
+
44
+ io.string[0...-2] # Strip trailing CRLF of last document
45
+ end
46
+
47
+ def self.encode64(data)
48
+ encoded = Base64.encode64(data).gsub /\n/, "\r\n"
49
+
50
+ crc = Digest::CRC24.checksum data
51
+
52
+ encoded << "=#{Base64.encode64([ crc ].pack("N")[1..-1])[0..-2]}"
53
+
54
+ encoded
55
+ end
56
+
57
+ def self.decode64(data)
58
+ lines = data.split "\r\n"
59
+
60
+ data = ""
61
+ crc = nil
62
+
63
+ lines.each do |line|
64
+ if line[0] == '='
65
+ crc, = "\0".concat(Base64.decode64(line[1..-1])).unpack('N')
66
+ else
67
+ data << line
68
+ end
69
+ end
70
+
71
+ data = Base64.decode64 data
72
+
73
+ if !crc.nil?
74
+ calculated_crc = Digest::CRC24.checksum data
75
+
76
+ raise "CyberplatPKI: CRYPT_ERR_RADIX_DECODE (invalid data checksum)" if calculated_crc != crc
77
+ end
78
+
79
+ data
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,104 @@
1
+ module CyberplatPKI
2
+ module DocumentIORoutines
3
+ def readline_crlf
4
+ line = readline "\r\n"
5
+
6
+ line.sub! /\r\n$/, ''
7
+
8
+ line
9
+ end
10
+
11
+ def read_key_id
12
+ line = readline_crlf
13
+
14
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (invalid key ID line: '#{line}'. Expecting 28 characters, got #{line.length})" unless line.length == 28
15
+
16
+ KeyId.new line[0...20].rstrip!, cut_int(line, 20...28)
17
+ end
18
+
19
+ def write_key_id(key_id)
20
+ printf "%-20s%08d\r\n", key_id.key_name, key_id.key_serial
21
+ end
22
+
23
+ def read_block(header, trailer, data_size)
24
+ line = readline_crlf
25
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (unexpected header '#{line}', expecting '#{header}')" if line != header
26
+
27
+ data_start = pos
28
+ seek data_size, IO::SEEK_CUR
29
+
30
+ line = readline_crlf
31
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (unexpected trailer '#{line}', expecting '')" if line != ''
32
+
33
+ line = readline_crlf
34
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (unexpected trailer '#{line}', expecting '#{trailer}')" if line != trailer
35
+
36
+ block_end = tell
37
+
38
+ seek data_start
39
+ data = read data_size
40
+ seek block_end
41
+
42
+ data
43
+ end
44
+
45
+ def write_block(header, trailer, data)
46
+ write "#{header}\r\n#{data}\r\n#{trailer}\r\n"
47
+ end
48
+
49
+ def read_document
50
+ document_start = pos
51
+
52
+ header = readline_crlf
53
+
54
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (invalid document header: '#{header}'. Expecting 36 characters, got #{header.length})" unless header.length == 36
55
+
56
+ doc = Document.new
57
+
58
+ length = cut_int header, 0...8 # Length of following data (e.g. document length - 8)
59
+ doc.engine = cut_int header, 8...10 # Engine ID
60
+ doc.type = header[10...12].to_sym # Document type
61
+ body_block = cut_int header, 12...20 # Body block size
62
+ doc.data_length = cut_int header, 20...28 # Length of body in signatures
63
+ signature_block = cut_int header, 28...36 # Signature block size
64
+
65
+ doc.subject = read_key_id
66
+ doc.ca = read_key_id
67
+
68
+ doc.body = read_block 'BEGIN', 'END', body_block
69
+ doc.signature = read_block 'BEGIN SIGNATURE', 'END SIGNATURE', signature_block
70
+
71
+ seek document_start + 8 + length + 2
72
+
73
+ doc
74
+ end
75
+
76
+ def write_document(doc)
77
+ start_pos = pos
78
+
79
+ printf "XXXXXXXX%02d%s%08d%08d%08d\r\n", doc.engine, doc.type.to_s, doc.body.length, doc.data_length, doc.signature.length
80
+ write_key_id doc.subject
81
+ write_key_id doc.ca
82
+
83
+ write_block 'BEGIN', 'END', doc.body
84
+ write_block 'BEGIN SIGNATURE', 'END SIGNATURE', doc.signature
85
+
86
+ end_pos = pos
87
+ seek start_pos
88
+ printf "%08d", end_pos - start_pos - 10
89
+ seek end_pos
90
+ end
91
+
92
+ private
93
+
94
+ def cut_int(string, range)
95
+ slice = string[range]
96
+
97
+ begin
98
+ Integer slice, 10
99
+ rescue ArgumentError => e
100
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (invalid integer in document: '#{slice}')"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,108 @@
1
+ require "crypt/idea"
2
+
3
+ module CyberplatPKI
4
+ class IdeaCfb
5
+ BLOCK_SIZE = 8
6
+
7
+ def initialize
8
+ @encbuf = "\0" * BLOCK_SIZE
9
+ @fr = "\0" * BLOCK_SIZE
10
+ @fre = "\0" * BLOCK_SIZE
11
+ @pos = 0
12
+ @keys = nil
13
+ end
14
+
15
+ def start_encryption(key)
16
+ @idea = Crypt::IDEA.new key.unpack('n*'), Crypt::IDEA::ENCRYPT
17
+ end
18
+
19
+ def start_decryption(key)
20
+ @idea = Crypt::IDEA.new key.unpack('n*'), Crypt::IDEA::DECRYPT
21
+ end
22
+
23
+ def encrypt(data)
24
+ cfb_engine data, method(:mix_enc), @idea.method(:encrypt_block)
25
+ end
26
+
27
+
28
+ def decrypt(data)
29
+ cfb_engine data, method(:mix_dec), @idea.method(:decrypt_block)
30
+ end
31
+
32
+ def resync
33
+ if @pos != 0
34
+ @fr[0...BLOCK_SIZE - @pos] = @encbuf[@pos...BLOCK_SIZE]
35
+ @fr[BLOCK_SIZE - @pos...BLOCK_SIZE] = @encbuf[0...@pos]
36
+
37
+ @encbuf = "\0" * BLOCK_SIZE
38
+ @pos = 0
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def idea_block(data)
45
+ data
46
+ end
47
+
48
+ def mix_enc(data)
49
+ outbuf = "\0" * data.length
50
+
51
+ (@pos...@pos + data.length).each_with_index do |i, index|
52
+ outbuf[index] = @encbuf[i] = (@fre[i].ord ^ data[index].ord).chr
53
+ end
54
+
55
+ @pos += data.length
56
+
57
+ outbuf
58
+ end
59
+
60
+ def mix_dec(data)
61
+ outbuf = "\0" * data.length
62
+
63
+ (@pos...@pos + data.length).each_with_index do |i, index|
64
+ @encbuf[i] = data[index]
65
+ outbuf[index] = (@fre[i].ord ^ @encbuf[i].ord).chr
66
+ end
67
+
68
+ @pos += data.length
69
+
70
+ outbuf
71
+ end
72
+
73
+ def cfb_engine(data, mix, crypt_block)
74
+ pos = 0
75
+ dst = ""
76
+
77
+ while pos < data.length && @pos > 0
78
+ n = [ BLOCK_SIZE - @pos, data.length - pos ].min
79
+
80
+ dst << mix.call(data[pos...pos + n])
81
+ pos += n
82
+
83
+ if @pos == BLOCK_SIZE
84
+ @fr = @encbuf
85
+ @encbuf = "\0" * BLOCK_SIZE
86
+ @pos = 0
87
+ end
88
+ end
89
+
90
+ while pos < data.length
91
+ @fre = crypt_block.call @fr
92
+
93
+ n = [ BLOCK_SIZE, data.length - pos ].min
94
+
95
+ dst << mix.call(data[pos...pos + n])
96
+ pos += n
97
+
98
+ if @pos == BLOCK_SIZE
99
+ @fr = @encbuf
100
+ @encbuf = "\0" * BLOCK_SIZE
101
+ @pos = 0
102
+ end
103
+ end
104
+
105
+ dst
106
+ end
107
+ end
108
+ end
@@ -1,78 +1,150 @@
1
+ require "openssl"
2
+
1
3
  module CyberplatPKI
2
4
  class Key
3
- attr_reader :internal
5
+ module Helpers
6
+ def self.key_from_document_set(list, type, serial, password = nil)
7
+ document = list.find do |doc|
8
+ doc.type == type && (serial == 0 || doc.subject.key_serial == serial)
9
+ end
4
10
 
5
- class << self
6
- def new_private(source, password, engine=Library::DEFAULT_ENGINE)
7
- internal = Library::Key.new
11
+ raise "CyberplatPKI: CRYPT_ERR_PUB_KEY_NOT_FOUND (key with specified serial has not been found in the document)" if document.nil?
8
12
 
9
- Library.invoke :OpenSecretKey,
10
- engine,
11
- source, source.length,
12
- password,
13
- internal
13
+ key = Key.new
14
+ key.serial = document.subject.key_serial
15
+ key.name = document.subject.key_name
16
+ key.packets = Packet.load Document.decode64(document.body), password
17
+ key.signature = Packet.load Document.decode64(document.signature), password
14
18
 
15
- new(internal)
19
+ [ key, document ]
16
20
  end
17
21
 
18
- def new_public(source, serial, ca_key=nil, engine=Library::DEFAULT_ENGINE)
19
- internal = Library::Key.new
22
+ def self.find_record(list, record_class)
23
+ record = list.find { |record| record.kind_of? record_class }
20
24
 
21
- if ca_key
22
- ca_key = ca_key.internal
23
- end
25
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (#{record_class.name} not found in the document)" if record.nil?
24
26
 
25
- Library.invoke :OpenPublicKey,
26
- engine,
27
- source, source.length,
28
- serial,
29
- internal,
30
- ca_key
31
-
32
- new(internal)
27
+ record
33
28
  end
29
+ end
34
30
 
35
- private :new
31
+ attr_accessor :serial, :name, :signature, :ca_key, :packets
32
+
33
+ def self.new_private(source, password = nil)
34
+ documents = Document.load source
35
+
36
+ key, document = Helpers.key_from_document_set documents, :NM, 0, password
37
+ key.ca_key = key
38
+
39
+ key
36
40
  end
37
41
 
38
- def initialize(internal)
39
- @internal = internal
40
- if defined?(ObjectSpace) &&
41
- ObjectSpace.respond_to?(:define_finalizer)
42
- ObjectSpace.define_finalizer(self, lambda {
43
- Library.invoke :CloseKey, internal
44
- })
42
+ def self.new_public(source, serial, ca_key = nil)
43
+ documents = Document.load source
44
+
45
+ key, document = Helpers.key_from_document_set documents, :NS, serial
46
+ if ca_key.nil?
47
+ if document.subject == document.ca
48
+ key.ca_key = key
49
+ else
50
+ key.ca_key, ca_document = Helpers.key_from_document_set documents, :NS, document.ca.key_serial
51
+ key.ca_key.ca_key = key.ca_key
52
+
53
+ key.ca_key.validate
54
+ end
45
55
  else
46
- warn "No ObjectSpace.define_finalizer; Crypt_CloseKey will not be called."
56
+ key.ca_key = ca_key
47
57
  end
58
+
59
+ key.validate
60
+
61
+ key
62
+ end
63
+
64
+ def initialize
65
+ @serial = nil
66
+ @name = nil
67
+ @signature = nil
68
+ @ca_key = nil
69
+ @packets = nil
48
70
  end
49
71
 
50
72
  def sign(data)
51
- # Be fucking optimistic. Someone, please teach the morons from
52
- # cyberplat how to design APIs and document them.
53
- # I sincerely hope this does not segfault in production.
54
- result = FFI::MemoryPointer.new(:char, data.length + 1024)
73
+ signature = SignaturePacket.new
74
+
75
+ signature.metadata = [
76
+ 0x01, # Signature type
77
+ serial, # Signing key serial number
78
+ Time.now.to_i # Timestamp
79
+ ].pack("CNN")
80
+
81
+ key_packet = Helpers.find_record packets, KeyPacket
55
82
 
56
- result_length = Library.invoke :Sign,
57
- data, data.size,
58
- result, result.total,
59
- @internal
83
+ digest = OpenSSL::Digest::MD5.new
84
+ signature.signature = key_packet.key.sign digest, data + signature.metadata
60
85
 
61
- result.read_string(result_length)
86
+ # Re-hash to get 'first word of digest'
87
+ digest.reset
88
+ digest.update data
89
+ digest.update signature.metadata
90
+ signature.hash_msw = digest.digest[0..1]
91
+
92
+ signature_block = Document.encode64 Packet.save([ signature ])
93
+
94
+ doc = Document.new
95
+ doc.engine = 1
96
+ doc.type = :SM
97
+ doc.subject = KeyId.new @name, @serial
98
+ doc.ca = KeyId.new '', 0
99
+ doc.data_length = data.length
100
+ doc.body = data
101
+ doc.signature = signature_block
102
+
103
+ text = Document.save [ doc ]
104
+
105
+ text
62
106
  end
63
107
 
64
108
  def verify(data_with_signature)
65
- retval = Library.Crypt_Verify \
66
- data_with_signature, data_with_signature.size,
67
- nil, nil,
68
- @internal
109
+ documents = Document.load data_with_signature
69
110
 
70
- if retval == -20 # VERIFY
71
- false
72
- else
73
- Library.handle_error("Crypt_Verify", retval)
74
- true
75
- end
111
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_FORMAT (expected one document of type SM)" if documents.length != 1 || documents[0].type != :SM
112
+
113
+ document, = *documents
114
+
115
+ signature_packet, = Packet.load Document.decode64(document.signature)
116
+ key_packet = Helpers.find_record packets, KeyPacket
117
+
118
+ digest = OpenSSL::Digest::MD5.new
119
+ signature = signature_packet.signature.ljust key_packet.key.n.num_bytes, 0.chr
120
+ key_packet.key.verify digest, signature, document.body + signature_packet.metadata
121
+ end
122
+
123
+ def validate
124
+ signature_packet = Helpers.find_record signature, SignaturePacket
125
+ key_packet = Helpers.find_record packets, KeyPacket
126
+ user_id_packet = Helpers.find_record packets, UserIdPacket
127
+ ca_key_packet = Helpers.find_record ca_key.packets, KeyPacket
128
+
129
+ io = StringIO.open ''.encode('BINARY'), 'wb'
130
+ io.extend PacketIORoutines
131
+
132
+ io.seek 3
133
+ key_packet.save io, nil
134
+
135
+ data_length = io.pos - 3
136
+ io.rewind
137
+ io.write [ 0x99, data_length ].pack "Cn"
138
+ io.seek 0, IO::SEEK_END
139
+
140
+ io.write user_id_packet.user_id
141
+ io.write signature_packet.metadata
142
+
143
+ digest = OpenSSL::Digest::MD5.new
144
+ signature = signature_packet.signature.rjust ca_key_packet.key.n.num_bytes, 0.chr
145
+ valid = ca_key_packet.key.verify digest, signature, io.string
146
+
147
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_KEY (key signature verification failed)" unless valid
76
148
  end
77
149
  end
78
- end
150
+ end
@@ -0,0 +1,15 @@
1
+ module CyberplatPKI
2
+ class KeyId
3
+ attr_accessor :key_name, :key_serial
4
+
5
+ def initialize(key_name = nil, key_serial = nil)
6
+ @key_name = key_name
7
+ @key_serial = key_serial
8
+ end
9
+
10
+ def ==(other)
11
+ key_name == other.key_name && key_serial == other.key_serial
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,45 @@
1
+ require "openssl"
2
+
3
+ module CyberplatPKI
4
+ class KeyPacket < Packet
5
+ attr_accessor :serial, :timestamp, :valid_days, :algorithm, :key
6
+
7
+ def self.load(io, context)
8
+ version = io.readbyte
9
+
10
+ # RFC4880 says:
11
+ #
12
+ # V3 keys are deprecated. They contain three weaknesses. First, it is
13
+ # relatively easy to construct a V3 key that has the same Key ID as any
14
+ # other key because the Key ID is simply the low 64 bits of the public
15
+ # modulus. Secondly, because the fingerprint of a V3 key hashes the
16
+ # key material, but not its length, there is an increased opportunity
17
+ # for fingerprint collisions. Third, there are weaknesses in the MD5
18
+ # hash algorithm that make developers prefer other algorithms. See
19
+ # below for a fuller discussion of Key IDs and fingerprints.
20
+ #
21
+ # Beware.
22
+
23
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (unsupported key version: #{version})" if version != 0x03
24
+
25
+ key = KeyPacket.new
26
+
27
+ key.serial, key.timestamp, key.valid_days, algorithm = io.read(11).unpack "NNnC"
28
+
29
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (unsupported algorithm #{algorithm})" if algorithm != 1
30
+
31
+ key.key = OpenSSL::PKey::RSA.new
32
+ key.key.n = io.read_mpi
33
+ key.key.e = io.read_mpi
34
+
35
+ key
36
+ end
37
+
38
+ def save(io, context)
39
+ io.write [ 3, serial, timestamp, valid_days, 1 ].pack("CNNnC")
40
+
41
+ io.write_mpi key.n
42
+ io.write_mpi key.e
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ require "stringio"
2
+
3
+ module CyberplatPKI
4
+ class Packet
5
+ def self.load(source, password = nil)
6
+ io = StringIO.new source, "rb"
7
+ io.extend PacketIORoutines
8
+
9
+ packets = []
10
+
11
+ until io.eof?
12
+ begin
13
+ packets << io.read_packet(password)
14
+ rescue EOFError => e
15
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (unexpected end of packet)"
16
+ end
17
+ end
18
+
19
+ packets
20
+ end
21
+
22
+ def self.save(packets, password = nil)
23
+ io = StringIO.new '', "wb"
24
+ io.extend PacketIORoutines
25
+
26
+ packets.each { |packet| io.write_packet packet, password }
27
+
28
+ io.string
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ require "stringio"
2
+ require "openssl"
3
+
4
+ module CyberplatPKI
5
+ module PacketIORoutines
6
+ attr_accessor :cipher, :checksum
7
+
8
+ def read_packet(password = nil)
9
+ header = readbyte
10
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (invalid packet header)" if (header & 0xC0) != 0x80
11
+
12
+ case header & 3
13
+ when 0
14
+ data_length = readbyte
15
+
16
+ when 1
17
+ data_length, = read(2).unpack 'n'
18
+
19
+ when 2
20
+ data_length, = read(4).unpack 'N'
21
+
22
+ else
23
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (invalid packet length type: #{header % 3}"
24
+ end
25
+
26
+ packet_type = (header >> 2) & 0x0F
27
+
28
+ data = read data_length
29
+ packet_class = PACKET_TYPES[packet_type]
30
+
31
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (unsupported packet type: #{packet_type})" if packet_class.nil?
32
+
33
+ StringIO.open(data, "rb") do |io|
34
+ io.extend PacketIORoutines
35
+ packet_class.load io, context_for_password(password)
36
+ end
37
+ end
38
+
39
+ def write_packet(packet, password = nil)
40
+ io = StringIO.open ''.encode('BINARY'), 'wb'
41
+ io.extend PacketIORoutines
42
+ packet.save io, context_for_password(password)
43
+
44
+ data = io.string
45
+
46
+ packet_type = (PACKET_TYPES.key(packet.class) << 2) | 0x80
47
+
48
+ case data.length
49
+ when 0x00..0xFF
50
+ putc packet_type
51
+ putc data.length
52
+
53
+ when 0x0100..0xFFFF
54
+ putc packet_type | 1
55
+
56
+ write [ data.length ].pack("n")
57
+
58
+ when 0x00010000..0xFFFFFFFF
59
+ putc packet_type | 2
60
+
61
+ write [ data.length ].pack("N")
62
+ end
63
+
64
+ write data
65
+ end
66
+
67
+ def read_mpi
68
+ header = read(2)
69
+
70
+ mpi_bits, = header.unpack("n")
71
+ data = read((mpi_bits + 7) / 8)
72
+
73
+ if !cipher.nil?
74
+ cipher.resync
75
+ data = cipher.decrypt data
76
+ end
77
+
78
+ add_checksum header
79
+ add_checksum data
80
+
81
+ OpenSSL::BN.new data, 2
82
+ end
83
+
84
+ def write_mpi(bn)
85
+ header = [ bn.num_bits ].pack("n")
86
+ data = bn.to_s 2
87
+
88
+ add_checksum header
89
+ add_checksum data
90
+
91
+ write header
92
+
93
+ if !cipher.nil?
94
+ cipher.resync
95
+ data = cipher.encrypt data
96
+ end
97
+
98
+ write data
99
+ end
100
+
101
+ private
102
+
103
+ def context_for_password(password)
104
+ if password.nil?
105
+ nil
106
+ else
107
+ context = IdeaCfb.new
108
+ context.start_encryption OpenSSL::Digest::MD5.digest(password) # yes, encryption
109
+
110
+ context
111
+ end
112
+ end
113
+
114
+ def add_checksum(string)
115
+ if !checksum.nil?
116
+ @checksum = (@checksum + string.sum(16)) & 0xFFFF
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,49 @@
1
+ module CyberplatPKI
2
+ class PrivateKeyPacket < KeyPacket
3
+ def self.load(io, context)
4
+ key = super
5
+
6
+ cipher = io.readbyte
7
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (unsupported private key cipher: #{cipher})" if cipher != 1 # IDEA-CFB + MD5
8
+
9
+ iv = io.read 8
10
+ context.decrypt iv
11
+
12
+ public_key = key.key
13
+ key.key = OpenSSL::PKey::RSA.new
14
+
15
+ io.cipher = context
16
+ io.checksum = 0
17
+
18
+ key.key.d = io.read_mpi
19
+ key.key.p = io.read_mpi
20
+ key.key.q = io.read_mpi
21
+ dummy = io.read_mpi
22
+
23
+ calculated_checksum = io.checksum
24
+
25
+ io.checksum = nil
26
+ io.cipher = nil
27
+
28
+ checksum, = io.read(2).unpack("n")
29
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PASSWD (invalid MPI checksum. Expected #{checksum.to_s 16}, calculated #{calculated_checksum.to_s 16})" if checksum != calculated_checksum
30
+
31
+ key.key.dmp1 = key.key.d % (key.key.p - 1)
32
+ key.key.dmq1 = key.key.d % (key.key.q - 1)
33
+ key.key.iqmp = key.key.q.mod_inverse key.key.p
34
+
35
+ # jruby-openssl requires public key parameters to be set LAST
36
+ key.key.n = public_key.n
37
+ key.key.e = public_key.e
38
+
39
+ key
40
+ end
41
+
42
+ def save(io, context)
43
+ super
44
+
45
+ raise NotImplementedError, "CyberplatPKI: PrivateKeyPacket#save is not implemented"
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ module CyberplatPKI
2
+ class SignaturePacket < Packet
3
+ attr_accessor :metadata, :signature, :hash_msw
4
+
5
+ def initialize
6
+ @metadata = nil
7
+ @signature = nil
8
+ @hash_msw = nil
9
+ end
10
+
11
+ def self.load(io, context)
12
+ version = io.readbyte
13
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (unsupported signature length: #{version})" if version != 3
14
+
15
+ metadata_length = io.readbyte
16
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (invalid metadata length: #{metadata_length})" if metadata_length > 32
17
+
18
+ signature = new
19
+
20
+ signature.metadata = io.read metadata_length
21
+
22
+ key_algorithm, hash_algorithm = io.read(2).unpack("CC")
23
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (invalid public key algorithm: #{key_algorithm})" if key_algorithm != 0x01
24
+ raise "CyberplatPKI: CRYPT_ERR_INVALID_PACKET_FORMAT (invalid hash key algorithm: #{key_algorithm})" if hash_algorithm != 0x01
25
+
26
+ signature.hash_msw = io.read(2)
27
+ signature_bits, = io.read(2).unpack("n")
28
+ signature.signature = io.read (signature_bits + 7) / 8
29
+
30
+ signature
31
+ end
32
+
33
+ def save(io, context)
34
+ io.write [ 3, metadata.length ].pack "CC"
35
+ io.write metadata
36
+ io.write [ 1, 1 ].pack "CC"
37
+ io.write hash_msw
38
+ io.write [ signature.length * 8 ].pack "n"
39
+ io.write signature
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module CyberplatPKI
2
+ class TrustPacket < Packet
3
+ attr_accessor :trust
4
+
5
+ def initialize(trust = nil)
6
+ @trust = trust
7
+ end
8
+
9
+ def self.load(io, context)
10
+ new io.read
11
+ end
12
+
13
+ def save(io, context)
14
+ io.write @trust
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module CyberplatPKI
2
+ class UserIdPacket < Packet
3
+ attr_accessor :user_id
4
+
5
+ def initialize(user_id = nil)
6
+ @user_id = user_id
7
+ end
8
+
9
+ def self.load(io, context)
10
+ new io.read
11
+ end
12
+
13
+ def save(io, context)
14
+ io.write @user_id
15
+ end
16
+ end
17
+ end
data/lib/cyberplat_pki.rb CHANGED
@@ -1,10 +1,23 @@
1
- module CyberplatPKI
2
- require_relative 'cyberplat_pki/library'
3
- require_relative 'cyberplat_pki/key'
4
-
5
- Library.invoke :Initialize
1
+ require_relative 'cyberplat_pki/key_id'
2
+ require_relative 'cyberplat_pki/document'
3
+ require_relative 'cyberplat_pki/document_io_routines'
4
+ require_relative 'cyberplat_pki/packet'
5
+ require_relative 'cyberplat_pki/signature_packet'
6
+ require_relative 'cyberplat_pki/key_packet'
7
+ require_relative 'cyberplat_pki/private_key_packet'
8
+ require_relative 'cyberplat_pki/trust_packet'
9
+ require_relative 'cyberplat_pki/user_id_packet'
10
+ require_relative 'cyberplat_pki/packet_io_routines'
11
+ require_relative 'cyberplat_pki/key'
12
+ require_relative 'cyberplat_pki/idea_cfb'
6
13
 
7
- at_exit {
8
- Library.invoke :Done
14
+ module CyberplatPKI
15
+ PACKET_TYPES = {
16
+ 2 => SignaturePacket,
17
+ 5 => PrivateKeyPacket,
18
+ 6 => KeyPacket,
19
+ 12 => TrustPacket,
20
+ 13 => UserIdPacket
9
21
  }
10
22
  end
23
+
data/spec/key_spec.rb CHANGED
@@ -19,7 +19,7 @@ describe CyberplatPKI::Key do
19
19
 
20
20
  lambda {
21
21
  CyberplatPKI::Key.new_public("foo", 17033)
22
- }.should raise_error(/PUB_KEY_NOT_FOUND/)
22
+ }.should raise_error(/INVALID_FORMAT/)
23
23
  end
24
24
 
25
25
  it "should load private keys" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cyberplat_pki
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,10 +9,26 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-09 00:00:00.000000000 Z
12
+ date: 2012-11-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: ffi
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: digest-crc
16
32
  requirement: !ruby/object:Gem::Requirement
17
33
  none: false
18
34
  requirements:
@@ -28,14 +44,14 @@ dependencies:
28
44
  - !ruby/object:Gem::Version
29
45
  version: '0'
30
46
  - !ruby/object:Gem::Dependency
31
- name: rspec
47
+ name: crypt
32
48
  requirement: !ruby/object:Gem::Requirement
33
49
  none: false
34
50
  requirements:
35
51
  - - ! '>='
36
52
  - !ruby/object:Gem::Version
37
53
  version: '0'
38
- type: :development
54
+ type: :runtime
39
55
  prerelease: false
40
56
  version_requirements: !ruby/object:Gem::Requirement
41
57
  none: false
@@ -43,7 +59,7 @@ dependencies:
43
59
  - - ! '>='
44
60
  - !ruby/object:Gem::Version
45
61
  version: '0'
46
- description: CyberplatPKI is an FFI binding for signing Cyberplat requests.
62
+ description: CyberplatPKI is a library for signing Cyberplat requests.
47
63
  email:
48
64
  - whitequark@whitequark.org
49
65
  executables: []
@@ -58,17 +74,23 @@ files:
58
74
  - README.md
59
75
  - Rakefile
60
76
  - cyberplat_pki.gemspec
61
- - ext/libipriv-linux32.so
62
- - ext/libipriv32.dll
63
77
  - lib/cyberplat_pki.rb
78
+ - lib/cyberplat_pki/document.rb
79
+ - lib/cyberplat_pki/document_io_routines.rb
80
+ - lib/cyberplat_pki/idea_cfb.rb
64
81
  - lib/cyberplat_pki/key.rb
65
- - lib/cyberplat_pki/library.rb
82
+ - lib/cyberplat_pki/key_id.rb
83
+ - lib/cyberplat_pki/key_packet.rb
84
+ - lib/cyberplat_pki/packet.rb
85
+ - lib/cyberplat_pki/packet_io_routines.rb
86
+ - lib/cyberplat_pki/private_key_packet.rb
87
+ - lib/cyberplat_pki/signature_packet.rb
88
+ - lib/cyberplat_pki/trust_packet.rb
89
+ - lib/cyberplat_pki/user_id_packet.rb
66
90
  - spec/key_spec.rb
67
91
  - spec/keys/pubkeys.key
68
92
  - spec/keys/secret.key
69
93
  - spec/spec_helper.rb
70
- - vendor/linux.zip
71
- - vendor/win32.zip
72
94
  homepage: http://github.com/whitequark/cyberplat_pki
73
95
  licenses: []
74
96
  post_install_message:
@@ -92,7 +114,7 @@ rubyforge_project:
92
114
  rubygems_version: 1.8.23
93
115
  signing_key:
94
116
  specification_version: 3
95
- summary: CyberplatPKI is an FFI binding for signing Cyberplat requests.
117
+ summary: CyberplatPKI is a library for signing Cyberplat requests.
96
118
  test_files:
97
119
  - spec/key_spec.rb
98
120
  - spec/keys/pubkeys.key
Binary file
data/ext/libipriv32.dll DELETED
Binary file
@@ -1,128 +0,0 @@
1
- require 'ffi'
2
-
3
- module CyberplatPKI::Library
4
- extend FFI::Library
5
-
6
- if defined?(Rubinius) # Fuck you.
7
- if Rubinius.windows?
8
- tuple = "windows-"
9
- elsif RUBY_PLATFORM =~ /linux/
10
- tuple = "linux-"
11
- else
12
- tuple = "unknown-"
13
- end
14
-
15
- if FFI::Platform::ARCH =~ /x86_64/
16
- tuple << "x86_64"
17
- elsif FFI::Platform::ARCH =~ /(i.?|x)86/
18
- tuple << "i386"
19
- end
20
- else
21
- tuple = "#{FFI::Platform::OS}-#{FFI::Platform::ARCH}"
22
- end
23
-
24
- if tuple == 'windows-i386'
25
- ffi_lib File.expand_path('../../../ext/libipriv32.dll', __FILE__)
26
- elsif tuple == 'linux-i386'
27
- ffi_lib File.expand_path('../../../ext/libipriv-linux32.so', __FILE__)
28
- else
29
- raise "CyberplatPKI: unsupported platform #{tuple}."
30
- end
31
-
32
- Errors = {
33
- -1 => "BAD_ARGS",
34
- -2 => "OUT_OF_MEMORY",
35
- -3 => "INVALID_FORMAT",
36
- -4 => "NO_DATA_FOUND",
37
- -5 => "INVALID_PACKET_FORMAT",
38
- -6 => "UNKNOWN_ALG",
39
- -7 => "INVALID_KEYLEN",
40
- -8 => "INVALID_PASSWD",
41
- -9 => "DOCTYPE",
42
- -10 => "RADIX_DECODE",
43
- -11 => "RADIX_ENCODE",
44
- -12 => "INVALID_ENG",
45
- -13 => "ENG_NOT_READY",
46
- -14 => "NOT_SUPPORT",
47
- -15 => "FILE_NOT_FOUND",
48
- -16 => "CANT_READ_FILE",
49
- -17 => "INVALID_KEY",
50
- -18 => "SEC_ENC",
51
- -19 => "PUB_KEY_NOT_FOUND",
52
- -20 => "VERIFY",
53
- -21 => "CREATE_FILE",
54
- -22 => "CANT_WRITE_FILE",
55
- -23 => "INVALID_KEYCARD",
56
- -24 => "GENKEY",
57
- -25 => "PUB_ENC",
58
- -26 => "SEC_DEC",
59
- }
60
-
61
- ENGINE_RSAREF = 0
62
- ENGINE_OPENSSL = 1
63
- ENGINE_PKCS11 = 2
64
- ENGINE_WINCRYPT = 3
65
-
66
- DEFAULT_ENGINE = ENGINE_RSAREF
67
-
68
- ENGCMD_IS_READY = 0
69
- ENGCMD_GET_ERROR = 1
70
- ENGCMD_SET_PIN = 2
71
- ENGCMD_SET_PKCS11_LIB = 3
72
- ENGCMD_GET_PKCS11_SLOTS_NUM = 4
73
- ENGCMD_GET_PKCS11_SLOT_NAME = 5
74
- ENGCMD_SET_PKCS11_SLOT = 6
75
- ENGCMD_ENUM_PKCS11_KEYS = 7
76
- ENGCMD_ENUM_PKCS11_PUBKEYS = 8
77
-
78
- KEY_TYPE_RSA_SECRET = 1
79
- KEY_TYPE_RSA_PUBLIC = 2
80
-
81
- MAX_USERID_LENGTH = 20
82
-
83
- class Key < FFI::Struct
84
- layout :eng, :short,
85
- :type, :short,
86
- :keyserial, :ulong,
87
- :userid, [:char, 24],
88
- :key, :pointer
89
- end
90
-
91
- # Only required functions are defined.
92
- # For all functions, zero return code is success, nonzero is error.
93
-
94
- # int Crypt_Initialize(void)
95
- attach_function :Crypt_Initialize, [], :int
96
-
97
- # int Crypt_OpenSecretKey(int eng, const char* src, int src_len, const char* password, IPRIV_KEY* key)
98
- attach_function :Crypt_OpenSecretKey, [:int, :string, :int, :string, :pointer], :int
99
-
100
- # int Crypt_OpenPublicKey(int eng, const char* src, int src_len, int keyserial, IPRIV_KEY* key, IPRIV_KEY* cakey)
101
- attach_function :Crypt_OpenPublicKey, [:int, :string, :int, :int, :pointer, :pointer], :int
102
-
103
- # int Crypt_Sign(const char* src, int src_len, char* dst, int dst_len, IPRIV_KEY* key);
104
- attach_function :Crypt_Sign, [:string, :int, :pointer, :int, :pointer], :int
105
-
106
- # int Crypt_Verify(const char* src, int src_len, const char** pdst, int* pndst,IPRIV_KEY* key);
107
- attach_function :Crypt_Verify, [:string, :int, :pointer, :pointer, :pointer], :int
108
-
109
- # int Crypt_CloseKey(IPRIV_KEY* key)
110
- attach_function :Crypt_CloseKey, [:pointer], :int
111
-
112
- # int Crypt_Done(void)
113
- attach_function :Crypt_Done, [], :int
114
-
115
- def self.handle_error(function, retval)
116
- if retval < 0
117
- error = Errors[retval] || "UNKNOWN"
118
- raise "CyberplatPKI: Cannot invoke #{function}: #{error} (#{retval})"
119
- end
120
-
121
- retval
122
- end
123
-
124
- def self.invoke(function, *args)
125
- function = :"Crypt_#{function}"
126
- handle_error(function, send(function, *args))
127
- end
128
- end
data/vendor/linux.zip DELETED
Binary file
data/vendor/win32.zip DELETED
Binary file