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 +4 -4
- data/lib/minisign/key_pair.rb +63 -0
- data/lib/minisign/private_key.rb +76 -48
- data/lib/minisign/public_key.rb +45 -14
- data/lib/minisign/signature.rb +7 -1
- data/lib/minisign/utils.rb +23 -1
- data/lib/minisign.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90f8322a99590a021707a0f91aa959090352ed35571364ea536b298a03896f06
|
4
|
+
data.tar.gz: 63f0cbc1604e551fc16e5bc4cd0456cbb2818cb30994bdd0aedc109bbb7e3dae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/minisign/private_key.rb
CHANGED
@@ -1,73 +1,101 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Minisign
|
4
|
-
#
|
4
|
+
# The private key used to create signatures
|
5
5
|
class PrivateKey
|
6
6
|
include Utils
|
7
|
-
attr_reader :
|
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(
|
14
|
+
# Minisign::PrivateKey.new(File.read("test/minisign.key"), 'password')
|
18
15
|
def initialize(str, password = nil)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@
|
24
|
-
@kdf_opslimit = bytes
|
25
|
-
@
|
26
|
-
|
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
|
-
#
|
55
|
-
|
56
|
-
|
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
|
-
#
|
60
|
-
|
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
|
data/lib/minisign/public_key.rb
CHANGED
@@ -1,49 +1,80 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Minisign
|
4
|
-
#
|
4
|
+
# The public key used to verify signatures
|
5
5
|
class PublicKey
|
6
6
|
include Utils
|
7
|
-
#
|
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
|
-
@
|
14
|
-
@
|
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
|
-
#
|
21
|
+
# public_key.key_id
|
21
22
|
# #=> "E86FECED695E8E0"
|
22
23
|
def key_id
|
23
|
-
|
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
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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: #{
|
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
|
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
|
data/lib/minisign/signature.rb
CHANGED
@@ -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
|
data/lib/minisign/utils.rb
CHANGED
@@ -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
|
-
|
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
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
|
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-
|
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
|