cyberplat_pki 1.0.0 → 2.0.0

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/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