diff --git a/CHANGELOG.md b/CHANGELOG.md index b0524d1..fe0fb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Support signing with unencrypted keys + ## [0.0.8] - 2024-02-03 ### Added diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index a6d0472..d3bc57b 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -9,6 +9,7 @@ class PrivateKey # rubocop:disable Metrics/AbcSize # rubocop:disable Layout/LineLength + # rubocop:disable Metrics/MethodLength # Parse signing information from the minisign private key # @@ -20,14 +21,31 @@ def initialize(str, password = nil) bytes = Base64.decode64(contents.last).bytes @signature_algorithm, @kdf_algorithm, @cksum_algorithm = [bytes[0..1], bytes[2..3], bytes[4..5]].map { |a| a.pack('U*') } + raise 'Missing password for encrypted key' if @kdf_algorithm.bytes.sum != 0 && password.nil? + @kdf_salt = bytes[6..37] @kdf_opslimit = bytes[38..45].pack('V*').unpack('N*').sum @kdf_memlimit = bytes[46..53].pack('V*').unpack('N*').sum - kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit) - @key_id, @secret_key, @public_key, @checksum = xor(kdf_output, bytes[54..157]) + key_data_bytes = if password + kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit) + xor(kdf_output, bytes[54..157]) + else + bytes[54..157] + end + @key_id, @secret_key, @public_key, @checksum = key_data(key_data_bytes) + assert_keypair_match! end # rubocop:enable Layout/LineLength # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + + def assert_keypair_match! + raise 'Wrong password for that key' if @public_key != ed25519_signing_key.verify_key.to_bytes.bytes + end + + def key_data(bytes) + [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] + end # @return [String] the used to xor the ed25519 keys def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit) @@ -45,10 +63,9 @@ def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit) # @return [Array<32 bit unsigned ints>] the byte array containing the key id, the secret and public ed25519 keys, and the checksum def xor(kdf_output, contents) # rubocop:enable Layout/LineLength - xored = kdf_output.each_with_index.map do |b, i| + kdf_output.each_with_index.map do |b, i| contents[i] ^ b end - [xored[0..7], xored[8..39], xored[40..71], xored[72..103]] end # @return [Ed25519::SigningKey] the ed25519 signing key diff --git a/spec/minisign/private_key_spec.rb b/spec/minisign/private_key_spec.rb index fc10354..4d4a02f 100644 --- a/spec/minisign/private_key_spec.rb +++ b/spec/minisign/private_key_spec.rb @@ -14,6 +14,18 @@ expect(@private_key.kdf_algorithm).to eq('Sc') end + it 'raises if the private key requires a password but is not supplied' do + expect do + Minisign::PrivateKey.new(File.read('test/minisign.key')) + end.to raise_error('Missing password for encrypted key') + end + + it 'raises if the password is incorrect for the private key' do + expect do + Minisign::PrivateKey.new(File.read('test/minisign.key'), 'not the right password') + end.to raise_error('Wrong password for that key') + end + it 'parses the cksum_algorithm' do expect(@private_key.cksum_algorithm).to eq('B2') end @@ -53,15 +65,25 @@ describe 'sign' do it 'signs a file' do - Dir.glob('test/generated/*').each { |file| File.delete(file) } - filename = "#{SecureRandom.uuid}.txt" - message = SecureRandom.uuid - File.write("test/generated/#{filename}", message) - signature = @private_key.sign(filename, message) - File.write("test/generated/#{filename}.minisig", signature) + @filename = 'encrypted-key.txt' + @message = SecureRandom.uuid + File.write("test/generated/#{@filename}", @message) + signature = @private_key.sign(@filename, @message) + File.write("test/generated/#{@filename}.minisig", signature) @signature = Minisign::Signature.new(signature) @public_key = Minisign::PublicKey.new('RWSmKaOrT6m3TGwjwBovgOmlhSbyBUw3hyhnSOYruHXbJa36xHr8rq2M') - expect(@public_key.verify(@signature, message)).to match('Signature and comment signature verified') + expect(@public_key.verify(@signature, @message)).to match('Signature and comment signature verified') + end + it 'signs a file with an unencrypted key' do + @filename = 'unencrypted-key.txt' + @message = SecureRandom.uuid + File.write("test/generated/#{@filename}", @message) + @unencrypted_private_key = Minisign::PrivateKey.new(File.read('test/unencrypted.key')) + signature = @unencrypted_private_key.sign(@filename, @message) + File.write("test/generated/#{@filename}.minisig", signature) + @signature = Minisign::Signature.new(signature) + @public_key = Minisign::PublicKey.new('RWT/N/MXaBIWRAPzfdEKqVRq9txskjf5qh7EbqMLVHjkNTGFazO3zMw2') + expect(@public_key.verify(@signature, @message)).to match('Signature and comment signature verified') end end end diff --git a/spec/verify.sh b/spec/verify.sh index 85bc17d..ac08840 100755 --- a/spec/verify.sh +++ b/spec/verify.sh @@ -4,10 +4,12 @@ if [[ "$OSTYPE" == "darwin"* ]]; then url="https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-macos.zip" curl -sL $url -o test/generated/minisign.zip unzip -o test/generated/minisign.zip -d test/generated - test/generated/minisign -Vm test/generated/*.txt -p test/minisign.pub else url="https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz" curl -sL $url -o test/generated/minisign.tar.gz tar -xvzf test/generated/minisign.tar.gz -C test/generated - test/generated/minisign-linux/x86_64/minisign -Vm test/generated/*.txt -p test/minisign.pub + mv test/generated/minisign-linux/x86_64/minisign test/generated/minisign fi + +test/generated/minisign -Vm test/generated/encrypted-key.txt -p test/minisign.pub || exit 1 +test/generated/minisign -Vm test/generated/unencrypted-key.txt -p test/unencrypted.pub || exit 1 diff --git a/test/unencrypted.key b/test/unencrypted.key new file mode 100644 index 0000000..32726d2 --- /dev/null +++ b/test/unencrypted.key @@ -0,0 +1,2 @@ +untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/zfzF2gSFkTKpvJz4Zf4HGG/b6OCfwWzUBt6qubTzn3j1XSgZwRHwQPzfdEKqVRq9txskjf5qh7EbqMLVHjkNTGFazO3zMw2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/test/unencrypted.pub b/test/unencrypted.pub new file mode 100644 index 0000000..dcfa5b2 --- /dev/null +++ b/test/unencrypted.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key 4416126817F337FF +RWT/N/MXaBIWRAPzfdEKqVRq9txskjf5qh7EbqMLVHjkNTGFazO3zMw2