minisign 0.0.7 → 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: 8dd11c61143149fd612a6c08a084a4e5831ec66f860c6a706edea18fc53bec00
4
- data.tar.gz: f7b6013996e7e72b35ad8c500e2bf5d24ebc2a9abe0d1d09bda86cfaca2d4ba8
3
+ metadata.gz: 90f8322a99590a021707a0f91aa959090352ed35571364ea536b298a03896f06
4
+ data.tar.gz: 63f0cbc1604e551fc16e5bc4cd0456cbb2818cb30994bdd0aedc109bbb7e3dae
5
5
  SHA512:
6
- metadata.gz: 615740c7d8fde14c2de494b2f7f9d28ebfaff2cb583210e29ee18573b97180d2a3d1ff3631085cdb53803d35a08cd31d01457c321a45d6d8b684849bcf69cb08
7
- data.tar.gz: d89f2cace36de94f4909420161a120888a929b3a097b00cbfa33e7081f7dcb15bacd93ca21be52df9f36009c5b3f1455b75dd49c29c99c3299d6130c8561a8b7
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
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ # The private key used to create signatures
5
+ class PrivateKey
6
+ include Utils
7
+ attr_reader :key_id
8
+
9
+ # Parse signing information from the minisign private key
10
+ #
11
+ # @param str [String] The minisign private key
12
+ # @param password [String] The password used to encrypt the private key
13
+ # @example
14
+ # Minisign::PrivateKey.new(File.read("test/minisign.key"), 'password')
15
+ def initialize(str, password = nil)
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!
24
+ end
25
+
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)
32
+ end
33
+
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)
41
+ signature = ed25519_signing_key.sign(blake2b512(message))
42
+ trusted_comment = comment || "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
43
+ global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}")
44
+ Minisign::Signature.new([
45
+ 'untrusted comment: <arbitrary text>',
46
+ Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"),
47
+ "trusted comment: #{trusted_comment}",
48
+ Base64.strict_encode64(global_signature),
49
+ ''
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*'))
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ # The public key used to verify signatures
5
+ class PublicKey
6
+ include Utils
7
+ # Read a minisign public key
8
+ #
9
+ # @param str [String] The minisign public key
10
+ # @example
11
+ # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM')
12
+ # # or from a file:
13
+ # Minisign::PublicKey.new(File.read('test/minisign.pub'))
14
+ def initialize(str)
15
+ @lines = str.split("\n")
16
+ @decoded = Base64.strict_decode64(@lines.last)
17
+ end
18
+
19
+ # @return [String] the key id
20
+ # @example
21
+ # public_key.key_id
22
+ # #=> "E86FECED695E8E0"
23
+ def key_id
24
+ key_id_binary_string.bytes.map { |c| c.to_s(16) }.reverse.join.upcase
25
+ end
26
+
27
+ # Verify a message's signature
28
+ #
29
+ # @param signature [Minisign::Signature]
30
+ # @param message [String] the content that was signed
31
+ # @return [String] the trusted comment
32
+ # @raise Ed25519::VerifyError on invalid signatures
33
+ # @raise RuntimeError on tampered trusted comments
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))
38
+ begin
39
+ ed25519_verify_key.verify(signature.trusted_comment_signature, signature.signature + signature.trusted_comment)
40
+ rescue Ed25519::VerifyError
41
+ raise 'Comment signature verification failed'
42
+ end
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"
49
+ end
50
+
51
+ private
52
+
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)
78
+ raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ # Parse a .minisig file's contents
5
+ class Signature
6
+ # @param str [String] The contents of the .minisig file
7
+ # @example
8
+ # Minisign::Signature.new(File.read('test/example.txt.minisig'))
9
+ def initialize(str)
10
+ @lines = str.split("\n")
11
+ end
12
+
13
+ # @return [String] the key id
14
+ # @example
15
+ # Minisign::Signature.new(File.read('test/example.txt.minisig')).key_id
16
+ # #=> "E86FECED695E8E0"
17
+ def key_id
18
+ encoded_signature[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
19
+ end
20
+
21
+ # @return [String] the trusted comment
22
+ # @example
23
+ # Minisign::Signature.new(File.read('test/example.txt.minisig')).trusted_comment
24
+ # #=> "timestamp:1653934067\tfile:example.txt\thashed"
25
+ def trusted_comment
26
+ @lines[2].split('trusted comment: ')[1]
27
+ end
28
+
29
+ # @return [String] the signature for the trusted comment
30
+ def trusted_comment_signature
31
+ Base64.decode64(@lines[3])
32
+ end
33
+
34
+ # @return [String] the global signature
35
+ def signature
36
+ encoded_signature[10..]
37
+ end
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
+
44
+ private
45
+
46
+ def encoded_signature
47
+ Base64.decode64(@lines[1])
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ # Helpers used in multiple classes
5
+ module Utils
6
+ def blake2b256(message)
7
+ RbNaCl::Hash::Blake2b.digest(message, { digest_size: 32 })
8
+ end
9
+
10
+ def blake2b512(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
30
+ end
31
+ end
32
+ end
data/lib/minisign.rb CHANGED
@@ -2,96 +2,10 @@
2
2
 
3
3
  require 'ed25519'
4
4
  require 'base64'
5
- require 'openssl'
5
+ require 'rbnacl'
6
6
 
7
- # `minisign` is a rubygem for verifying {https://jedisct1.github.io/minisign minisign} signatures.
8
- # @author Jesse Shawl
9
- module Minisign
10
- # Parse a .minisig file's contents
11
- class Signature
12
- # @param str [String] The contents of the .minisig file
13
- # @example
14
- # Minisign::Signature.new(File.read('test/example.txt.minisig'))
15
- def initialize(str)
16
- @lines = str.split("\n")
17
- end
18
-
19
- # @return [String] the key id
20
- # @example
21
- # Minisign::Signature.new(File.read('test/example.txt.minisig')).key_id
22
- # #=> "E86FECED695E8E0"
23
- def key_id
24
- encoded_signature[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
25
- end
26
-
27
- # @return [String] the trusted comment
28
- # @example
29
- # Minisign::Signature.new(File.read('test/example.txt.minisig')).trusted_comment
30
- # #=> "timestamp:1653934067\tfile:example.txt\thashed"
31
- def trusted_comment
32
- @lines[2].split('trusted comment: ')[1]
33
- end
34
-
35
- def trusted_comment_signature
36
- Base64.decode64(@lines[3])
37
- end
38
-
39
- # @return [String] the signature
40
- def signature
41
- encoded_signature[10..]
42
- end
43
-
44
- private
45
-
46
- def encoded_signature
47
- Base64.decode64(@lines[1])
48
- end
49
- end
50
-
51
- # Parse ed25519 verify key from minisign public key
52
- class PublicKey
53
- # Parse the ed25519 verify key from the minisign public key
54
- #
55
- # @param str [String] The minisign public key
56
- # @example
57
- # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM')
58
- def initialize(str)
59
- @decoded = Base64.strict_decode64(str)
60
- @public_key = @decoded[10..]
61
- @verify_key = Ed25519::VerifyKey.new(@public_key)
62
- end
63
-
64
- # @return [String] the key id
65
- # @example
66
- # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM').key_id
67
- # #=> "E86FECED695E8E0"
68
- def key_id
69
- @decoded[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
70
- end
71
-
72
- # Verify a message's signature
73
- #
74
- # @param sig [Minisign::Signature]
75
- # @param message [String] the content that was signed
76
- # @return [String] the trusted comment
77
- # @raise Ed25519::VerifyError on invalid signatures
78
- # @raise RuntimeError on tampered trusted comments
79
- def verify(sig, message)
80
- blake = OpenSSL::Digest.new('BLAKE2b512')
81
- ensure_matching_key_ids(sig.key_id, key_id)
82
- @verify_key.verify(sig.signature, blake.digest(message))
83
- begin
84
- @verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment)
85
- rescue Ed25519::VerifyError
86
- raise 'Comment signature verification failed'
87
- end
88
- "Signature and comment signature verified\nTrusted comment: #{sig.trusted_comment}"
89
- end
90
-
91
- private
92
-
93
- def ensure_matching_key_ids(key_id1, key_id2)
94
- raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2
95
- end
96
- end
97
- end
7
+ require 'minisign/utils'
8
+ require 'minisign/public_key'
9
+ require 'minisign/signature'
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.7
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: 2022-06-22 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
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rbnacl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.1'
27
41
  description: Verify minisign signatures
28
42
  email: jesse@jesse.sh
29
43
  executables: []
@@ -31,6 +45,11 @@ extensions: []
31
45
  extra_rdoc_files: []
32
46
  files:
33
47
  - lib/minisign.rb
48
+ - lib/minisign/key_pair.rb
49
+ - lib/minisign/private_key.rb
50
+ - lib/minisign/public_key.rb
51
+ - lib/minisign/signature.rb
52
+ - lib/minisign/utils.rb
34
53
  homepage: https://rubygems.org/gems/minisign
35
54
  licenses:
36
55
  - MIT
@@ -44,14 +63,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
44
63
  requirements:
45
64
  - - ">="
46
65
  - !ruby/object:Gem::Version
47
- version: 2.6.0
66
+ version: '2.7'
48
67
  required_rubygems_version: !ruby/object:Gem::Requirement
49
68
  requirements:
50
69
  - - ">="
51
70
  - !ruby/object:Gem::Version
52
71
  version: '0'
53
72
  requirements: []
54
- rubygems_version: 3.0.3.1
73
+ rubygems_version: 3.1.6
55
74
  signing_key:
56
75
  specification_version: 4
57
76
  summary: Minisign, in Ruby!