minisign 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fff86c56cc4091b4517b01304262f097451c801847f4593d1bb99df4bef760f
4
- data.tar.gz: 4c3ffd93d6dd729a25b6eabd9773d3a226e27c1066329972132d87ab5b5d49cc
3
+ metadata.gz: 90f8322a99590a021707a0f91aa959090352ed35571364ea536b298a03896f06
4
+ data.tar.gz: 63f0cbc1604e551fc16e5bc4cd0456cbb2818cb30994bdd0aedc109bbb7e3dae
5
5
  SHA512:
6
- metadata.gz: 2bf06fbf6e88b3a003d5b62d245e8085f749a24a36022b66b4fa6d7f7901b5e2eececc0c107dd0ee1969cb4fa09dd28212695f401f1b8f4e5e2c8cc6adf85671
7
- data.tar.gz: cf5da0f974244ea824bd8ffd5a3f41dd875eac858c213429918a1b0da1445b35a9a479ac8ed1131b33eaaab7ad609dd6cd6dc98852b2e29afff83cb389327acd
6
+ metadata.gz: cf80345096982044eb942d0ddafaf9f1546006d543cff34b7ad424e962f930229a6241958627f2339d76a893dbcfb4b5266a88b056c98b3e491e1be7877ab66f
7
+ data.tar.gz: cbd7a29a71c353745fc6ffe95259368bf959c3418bfd32fae4cce1ede6baa1e449cf27bebc81410aacd2451d8284acd52448860a240d3ea4864ab53b3142f6a3
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ # Generate a Minisign secret and public key
5
+ class KeyPair
6
+ include Minisign::Utils
7
+
8
+ def initialize(password = nil)
9
+ @password = password
10
+ @key_id = SecureRandom.bytes(8)
11
+ @signing_key = Ed25519::SigningKey.generate
12
+
13
+ @checksum = blake2b256("Ed#{key_data}")
14
+ @keynum_sk = "#{key_data}#{@checksum}"
15
+
16
+ @kdf_salt = SecureRandom.bytes(32)
17
+ @keynum_sk = xor(kdf_output, @keynum_sk.bytes).pack('C*') if @password
18
+ @kdf_algorithm = password.nil? ? [0, 0].pack('U*') : 'Sc'
19
+ end
20
+
21
+ # @return [Minisign::PrivateKey]
22
+ def private_key
23
+ @kdf_opslimit = kdf_opslimit_bytes.pack('C*')
24
+ @kdf_memlimit = kdf_memlimit_bytes.pack('C*')
25
+ data = "Ed#{@kdf_algorithm}B2#{@kdf_salt}#{@kdf_opslimit}#{@kdf_memlimit}#{@keynum_sk}"
26
+ Minisign::PrivateKey.new(
27
+ "untrusted comment: minisign secret key\n#{Base64.strict_encode64(data)}",
28
+ @password
29
+ )
30
+ end
31
+
32
+ # @return [Minisign::PublicKey]
33
+ def public_key
34
+ data = Base64.strict_encode64("Ed#{@key_id}#{@signing_key.verify_key.to_bytes}")
35
+ Minisign::PublicKey.new(data)
36
+ end
37
+
38
+ private
39
+
40
+ def kdf_output
41
+ derive_key(
42
+ @password,
43
+ @kdf_salt,
44
+ kdf_opslimit_bytes.pack('V*').unpack('N*').sum,
45
+ kdf_memlimit_bytes.pack('V*').unpack('N*').sum
46
+ )
47
+ end
48
+
49
+ def key_data
50
+ @key_data ||= "#{@key_id}#{@signing_key.to_bytes}#{@signing_key.verify_key.to_bytes}"
51
+ end
52
+
53
+ # 🤷
54
+ # https://github.com/RubyCrypto/rbnacl/blob/3e8d8f8822e2b8eeba215e6be027e8ee210edfb9/lib/rbnacl/password_hash/scrypt.rb#L33-L34
55
+ def kdf_opslimit_bytes
56
+ [0, 0, 0, 2, 0, 0, 0, 0]
57
+ end
58
+
59
+ def kdf_memlimit_bytes
60
+ [0, 0, 0, 64, 0, 0, 0, 0]
61
+ end
62
+ end
63
+ end
@@ -1,73 +1,101 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Minisign
4
- # Parse ed25519 signing key from minisign private key
4
+ # The private key used to create signatures
5
5
  class PrivateKey
6
6
  include Utils
7
- attr_reader :signature_algorithm, :kdf_algorithm, :cksum_algorithm, :kdf_salt, :kdf_opslimit, :kdf_memlimit,
8
- :key_id, :public_key, :secret_key, :checksum
9
-
10
- # rubocop:disable Metrics/AbcSize
11
- # rubocop:disable Layout/LineLength
7
+ attr_reader :key_id
12
8
 
13
9
  # Parse signing information from the minisign private key
14
10
  #
15
11
  # @param str [String] The minisign private key
12
+ # @param password [String] The password used to encrypt the private key
16
13
  # @example
17
- # Minisign::PrivateKey.new('RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', 'password')
14
+ # Minisign::PrivateKey.new(File.read("test/minisign.key"), 'password')
18
15
  def initialize(str, password = nil)
19
- contents = str.split("\n")
20
- bytes = Base64.decode64(contents.last).bytes
21
- @signature_algorithm, @kdf_algorithm, @cksum_algorithm =
22
- [bytes[0..1], bytes[2..3], bytes[4..5]].map { |a| a.pack('U*') }
23
- @kdf_salt = bytes[6..37]
24
- @kdf_opslimit = bytes[38..45].pack('V*').unpack('N*').sum
25
- @kdf_memlimit = bytes[46..53].pack('V*').unpack('N*').sum
26
- kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit)
27
- @key_id, @secret_key, @public_key, @checksum = xor(kdf_output, bytes[54..157])
28
- end
29
- # rubocop:enable Layout/LineLength
30
- # rubocop:enable Metrics/AbcSize
31
-
32
- # @return [String] the <kdf_output> used to xor the ed25519 keys
33
- def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
34
- RbNaCl::PasswordHash.scrypt(
35
- password,
36
- kdf_salt.pack('C*'),
37
- kdf_opslimit,
38
- kdf_memlimit,
39
- 104
40
- ).bytes
41
- end
42
-
43
- # rubocop:disable Layout/LineLength
44
-
45
- # @return [Array<32 bit unsigned ints>] the byte array containing the key id, the secret and public ed25519 keys, and the checksum
46
- def xor(kdf_output, contents)
47
- # rubocop:enable Layout/LineLength
48
- xored = kdf_output.each_with_index.map do |b, i|
49
- contents[i] ^ b
50
- end
51
- [xored[0..7], xored[8..39], xored[40..71], xored[72..103]]
16
+ comment, data = str.split("\n")
17
+ @password = password
18
+ decoded = Base64.decode64(data)
19
+ @untrusted_comment = comment.split('untrusted comment: ').last
20
+ @bytes = decoded.bytes
21
+ @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes)
22
+ @key_id, @ed25519_private_key_bytes, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157])
23
+ assert_valid_key!
52
24
  end
53
25
 
54
- # @return [Ed25519::SigningKey] the ed25519 signing key
55
- def ed25519_signing_key
56
- Ed25519::SigningKey.new(@secret_key.pack('C*'))
26
+ # Get the corresponding public key from the private key
27
+ #
28
+ # @return [Minisign::PublicKey]
29
+ def public_key
30
+ data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key_bytes.pack('C*')}")
31
+ Minisign::PublicKey.new(data)
57
32
  end
58
33
 
59
- # @return [String] the signature in the .minisig format that can be written to a file.
60
- def sign(filename, message)
34
+ # Sign a file/message
35
+ #
36
+ # @param filename [String] The filename to be used in the trusted comment section
37
+ # @param message [String] The file's contents
38
+ # @param comment [String] An optional trusted comment to be included in the signature
39
+ # @return [Minisign::Signature]
40
+ def sign(filename, message, comment = nil)
61
41
  signature = ed25519_signing_key.sign(blake2b512(message))
62
- trusted_comment = "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
42
+ trusted_comment = comment || "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
63
43
  global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}")
64
- [
44
+ Minisign::Signature.new([
65
45
  'untrusted comment: <arbitrary text>',
66
46
  Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"),
67
47
  "trusted comment: #{trusted_comment}",
68
48
  Base64.strict_encode64(global_signature),
69
49
  ''
70
- ].join("\n")
50
+ ].join("\n"))
51
+ end
52
+
53
+ # @return [String] A string in the minisign.pub format
54
+ def to_s
55
+ kdf_salt = @kdf_salt.pack('C*')
56
+ kdf_opslimit = [@kdf_opslimit, 0].pack('L*')
57
+ kdf_memlimit = [@kdf_memlimit, 0].pack('L*')
58
+ keynum_sk = key_data(@password,
59
+ @key_id + @ed25519_private_key_bytes + @ed25519_public_key_bytes + @checksum).flatten
60
+ data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.pack('C*')}"
61
+ "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n"
62
+ end
63
+
64
+ private
65
+
66
+ def signature_algorithm
67
+ @bytes[0..1].pack('U*')
68
+ end
69
+
70
+ def cksum_algorithm
71
+ @bytes[4..5].pack('U*')
72
+ end
73
+
74
+ def kdf_algorithm
75
+ @bytes[2..3].pack('U*')
76
+ end
77
+
78
+ def scrypt_params(bytes)
79
+ [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum]
80
+ end
81
+
82
+ # @raise [RuntimeError] if the extracted public key does not match the derived public key
83
+ def assert_valid_key!
84
+ raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil?
85
+ raise 'Wrong password for that key' if @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes
86
+ end
87
+
88
+ def key_data(password, bytes)
89
+ if password
90
+ kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit)
91
+ bytes = xor(kdf_output, bytes)
92
+ end
93
+ [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]]
94
+ end
95
+
96
+ # @return [Ed25519::SigningKey] the ed25519 signing key
97
+ def ed25519_signing_key
98
+ Ed25519::SigningKey.new(@ed25519_private_key_bytes.pack('C*'))
71
99
  end
72
100
  end
73
101
  end
@@ -1,49 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Minisign
4
- # Parse ed25519 verify key from minisign public key
4
+ # The public key used to verify signatures
5
5
  class PublicKey
6
6
  include Utils
7
- # Parse the ed25519 verify key from the minisign public key
7
+ # Read a minisign public key
8
8
  #
9
9
  # @param str [String] The minisign public key
10
10
  # @example
11
11
  # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM')
12
+ # # or from a file:
13
+ # Minisign::PublicKey.new(File.read('test/minisign.pub'))
12
14
  def initialize(str)
13
- @decoded = Base64.strict_decode64(str)
14
- @public_key = @decoded[10..]
15
- @verify_key = Ed25519::VerifyKey.new(@public_key)
15
+ @lines = str.split("\n")
16
+ @decoded = Base64.strict_decode64(@lines.last)
16
17
  end
17
18
 
18
19
  # @return [String] the key id
19
20
  # @example
20
- # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM').key_id
21
+ # public_key.key_id
21
22
  # #=> "E86FECED695E8E0"
22
23
  def key_id
23
- @decoded[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
24
+ key_id_binary_string.bytes.map { |c| c.to_s(16) }.reverse.join.upcase
24
25
  end
25
26
 
26
27
  # Verify a message's signature
27
28
  #
28
- # @param sig [Minisign::Signature]
29
+ # @param signature [Minisign::Signature]
29
30
  # @param message [String] the content that was signed
30
31
  # @return [String] the trusted comment
31
32
  # @raise Ed25519::VerifyError on invalid signatures
32
33
  # @raise RuntimeError on tampered trusted comments
33
- def verify(sig, message)
34
- ensure_matching_key_ids(sig.key_id, key_id)
35
- @verify_key.verify(sig.signature, blake2b512(message))
34
+ # @raise RuntimeError on mismatching key ids
35
+ def verify(signature, message)
36
+ assert_matching_key_ids!(signature.key_id, key_id)
37
+ ed25519_verify_key.verify(signature.signature, blake2b512(message))
36
38
  begin
37
- @verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment)
39
+ ed25519_verify_key.verify(signature.trusted_comment_signature, signature.signature + signature.trusted_comment)
38
40
  rescue Ed25519::VerifyError
39
41
  raise 'Comment signature verification failed'
40
42
  end
41
- "Signature and comment signature verified\nTrusted comment: #{sig.trusted_comment}"
43
+ "Signature and comment signature verified\nTrusted comment: #{signature.trusted_comment}"
44
+ end
45
+
46
+ # @return [String] The public key that can be written to a file
47
+ def to_s
48
+ "untrusted comment: #{untrusted_comment}\n#{key_data}\n"
42
49
  end
43
50
 
44
51
  private
45
52
 
46
- def ensure_matching_key_ids(key_id1, key_id2)
53
+ def untrusted_comment
54
+ if @lines.length == 1
55
+ "minisign public key #{key_id}"
56
+ else
57
+ @lines.first.split('untrusted comment: ').last
58
+ end
59
+ end
60
+
61
+ def key_id_binary_string
62
+ @decoded[2..9]
63
+ end
64
+
65
+ def ed25519_public_key_binary_string
66
+ @decoded[10..]
67
+ end
68
+
69
+ def ed25519_verify_key
70
+ Ed25519::VerifyKey.new(ed25519_public_key_binary_string)
71
+ end
72
+
73
+ def key_data
74
+ Base64.strict_encode64("Ed#{key_id_binary_string}#{ed25519_public_key_binary_string}")
75
+ end
76
+
77
+ def assert_matching_key_ids!(key_id1, key_id2)
47
78
  raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2
48
79
  end
49
80
  end
@@ -26,15 +26,21 @@ module Minisign
26
26
  @lines[2].split('trusted comment: ')[1]
27
27
  end
28
28
 
29
+ # @return [String] the signature for the trusted comment
29
30
  def trusted_comment_signature
30
31
  Base64.decode64(@lines[3])
31
32
  end
32
33
 
33
- # @return [String] the signature
34
+ # @return [String] the global signature
34
35
  def signature
35
36
  encoded_signature[10..]
36
37
  end
37
38
 
39
+ # @return [String] The signature that can be written to a file
40
+ def to_s
41
+ "#{@lines.join("\n")}\n"
42
+ end
43
+
38
44
  private
39
45
 
40
46
  def encoded_signature
@@ -3,8 +3,30 @@
3
3
  module Minisign
4
4
  # Helpers used in multiple classes
5
5
  module Utils
6
+ def blake2b256(message)
7
+ RbNaCl::Hash::Blake2b.digest(message, { digest_size: 32 })
8
+ end
9
+
6
10
  def blake2b512(message)
7
- OpenSSL::Digest.new('BLAKE2b512').digest(message)
11
+ RbNaCl::Hash::Blake2b.digest(message, { digest_size: 64 })
12
+ end
13
+
14
+ # @return [Array<32 bit unsigned ints>]
15
+ def xor(kdf_output, contents)
16
+ kdf_output.each_with_index.map do |b, i|
17
+ contents[i] ^ b
18
+ end
19
+ end
20
+
21
+ # @return [String] the <kdf_output> used to xor the ed25519 keys
22
+ def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
23
+ RbNaCl::PasswordHash.scrypt(
24
+ password,
25
+ kdf_salt,
26
+ kdf_opslimit,
27
+ kdf_memlimit,
28
+ 104
29
+ ).bytes
8
30
  end
9
31
  end
10
32
  end
data/lib/minisign.rb CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  require 'ed25519'
4
4
  require 'base64'
5
- require 'openssl'
6
5
  require 'rbnacl'
7
6
 
8
7
  require 'minisign/utils'
9
8
  require 'minisign/public_key'
10
9
  require 'minisign/signature'
11
10
  require 'minisign/private_key'
11
+ require 'minisign/key_pair'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minisign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Shawl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-03 00:00:00.000000000 Z
11
+ date: 2024-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ed25519
@@ -45,6 +45,7 @@ extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
47
  - lib/minisign.rb
48
+ - lib/minisign/key_pair.rb
48
49
  - lib/minisign/private_key.rb
49
50
  - lib/minisign/public_key.rb
50
51
  - lib/minisign/signature.rb