minisign 0.0.7 → 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: 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!