minisign 0.0.8 → 0.1.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.
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