From 3397ada8c7336bc63bceb4adaa214222cbe8243a Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Sat, 3 Feb 2024 16:58:18 -0600 Subject: [PATCH] create signature (#5) --- .github/workflows/ruby.yml | 2 ++ .gitignore | 2 ++ CHANGELOG.md | 12 ++++++++++- README.md | 12 +++++++++++ lib/minisign.rb | 1 + lib/minisign/private_key.rb | 33 +++++++++++++++++++++++++++++++ lib/minisign/public_key.rb | 4 ++-- lib/minisign/utils.rb | 10 ++++++++++ spec/minisign/private_key_spec.rb | 22 +++++++++++++++++---- spec/minisign/signature_spec.rb | 8 ++++++++ spec/minisign_spec.rb | 7 ------- spec/verify.sh | 13 ++++++++++++ test/generated/.keep | 0 13 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 lib/minisign/utils.rb create mode 100644 spec/minisign/signature_spec.rb create mode 100755 spec/verify.sh create mode 100644 test/generated/.keep diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index c8c5fa7..9cf298c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -38,3 +38,5 @@ jobs: run: bundle exec rubocop - name: Run tests run: bundle exec rspec + - name: Test against jedisct1/minisign + run: ./spec/verify.sh diff --git a/.gitignore b/.gitignore index 9804fa1..b4e67cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ coverage doc .yardoc +test/generated/* +!test/generated/.keep diff --git a/CHANGELOG.md b/CHANGELOG.md index dc6d4cc..590377d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Verify key id match +- Create signatures - Parse private key - Use ruby 2.7 + +## [0.0.7] - 2022-06-22 + +### Changed +- Update bundler version + +## [0.0.6] - 2022-06-22 + +### Added +- Verify key id match ## [0.0.5] - 2022-05-30 diff --git a/README.md b/README.md index b2fd1ed..e50cab0 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,18 @@ The above is equivalent to: minisign -Vm test/example.txt -P RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM ``` +### Create a signature + +```rb +require 'minisign' +file_path = "example.txt" +password = "password" +private_key = Minisign::PrivateKey.new(File.read("minisign.key"), password) +signature = private_key.sign(file_path, File.read(file_path)) + +File.write("#{file_path}.minisig", signature.to_s) +``` + ## Local Development ``` diff --git a/lib/minisign.rb b/lib/minisign.rb index f898a65..eb6ab81 100644 --- a/lib/minisign.rb +++ b/lib/minisign.rb @@ -5,6 +5,7 @@ require 'openssl' require 'rbnacl' +require 'minisign/utils' require 'minisign/public_key' require 'minisign/signature' require 'minisign/private_key' diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index e895f0a..a6d0472 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -3,10 +3,18 @@ module Minisign # Parse ed25519 signing key from minisign private key class PrivateKey + include Utils attr_reader :signature_algorithm, :kdf_algorithm, :cksum_algorithm, :kdf_salt, :kdf_opslimit, :kdf_memlimit, :key_id, :public_key, :secret_key, :checksum # rubocop:disable Metrics/AbcSize + # rubocop:disable Layout/LineLength + + # Parse signing information from the minisign private key + # + # @param str [String] The minisign private key + # @example + # Minisign::PrivateKey.new('RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', 'password') def initialize(str, password = nil) contents = str.split("\n") bytes = Base64.decode64(contents.last).bytes @@ -18,8 +26,10 @@ def initialize(str, password = nil) kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit) @key_id, @secret_key, @public_key, @checksum = xor(kdf_output, bytes[54..157]) end + # rubocop:enable Layout/LineLength # rubocop:enable Metrics/AbcSize + # @return [String] the used to xor the ed25519 keys def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit) RbNaCl::PasswordHash.scrypt( password, @@ -30,11 +40,34 @@ def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit) ).bytes end + # rubocop:disable Layout/LineLength + + # @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| contents[i] ^ b end [xored[0..7], xored[8..39], xored[40..71], xored[72..103]] end + + # @return [Ed25519::SigningKey] the ed25519 signing key + def ed25519_signing_key + Ed25519::SigningKey.new(@secret_key.pack('C*')) + end + + # @return [String] the signature in the .minisig format that can be written to a file. + def sign(filename, message) + signature = ed25519_signing_key.sign(blake2b512(message)) + trusted_comment = "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed" + global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}") + [ + 'untrusted comment: ', + Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"), + "trusted comment: #{trusted_comment}", + Base64.strict_encode64(global_signature), + '' + ].join("\n") + end end end diff --git a/lib/minisign/public_key.rb b/lib/minisign/public_key.rb index 39e7355..9fe9550 100644 --- a/lib/minisign/public_key.rb +++ b/lib/minisign/public_key.rb @@ -3,6 +3,7 @@ module Minisign # Parse ed25519 verify key from minisign public key class PublicKey + include Utils # Parse the ed25519 verify key from the minisign public key # # @param str [String] The minisign public key @@ -30,9 +31,8 @@ def key_id # @raise Ed25519::VerifyError on invalid signatures # @raise RuntimeError on tampered trusted comments def verify(sig, message) - blake = OpenSSL::Digest.new('BLAKE2b512') ensure_matching_key_ids(sig.key_id, key_id) - @verify_key.verify(sig.signature, blake.digest(message)) + @verify_key.verify(sig.signature, blake2b512(message)) begin @verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment) rescue Ed25519::VerifyError diff --git a/lib/minisign/utils.rb b/lib/minisign/utils.rb new file mode 100644 index 0000000..d51b63a --- /dev/null +++ b/lib/minisign/utils.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Minisign + # Helpers used in multiple classes + module Utils + def blake2b512(message) + OpenSSL::Digest.new('BLAKE2b512').digest(message) + end + end +end diff --git a/spec/minisign/private_key_spec.rb b/spec/minisign/private_key_spec.rb index fc7a139..fc10354 100644 --- a/spec/minisign/private_key_spec.rb +++ b/spec/minisign/private_key_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true describe Minisign::PrivateKey do - describe '.new' do - before(:all) do - @private_key = Minisign::PrivateKey.new(File.read('test/minisign.key'), 'password') - end + before(:all) do + @private_key = Minisign::PrivateKey.new(File.read('test/minisign.key'), 'password') + end + describe '.new' do it 'parses the signature_algorithm' do expect(@private_key.signature_algorithm).to eq('Ed') end @@ -50,4 +50,18 @@ 113, 255, 174, 47, 39, 216, 61, 198, 233, 161, 233, 143, 84, 246, 255, 150]) end end + + 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) + @signature = Minisign::Signature.new(signature) + @public_key = Minisign::PublicKey.new('RWSmKaOrT6m3TGwjwBovgOmlhSbyBUw3hyhnSOYruHXbJa36xHr8rq2M') + expect(@public_key.verify(@signature, message)).to match('Signature and comment signature verified') + end + end end diff --git a/spec/minisign/signature_spec.rb b/spec/minisign/signature_spec.rb new file mode 100644 index 0000000..11829ec --- /dev/null +++ b/spec/minisign/signature_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +describe Minisign::Signature do + it 'has a key id' do + @signature = Minisign::Signature.new(File.read('test/example.txt.minisig')) + expect(@signature.key_id).to eq('4CB7A94FABA329A6') + end +end diff --git a/spec/minisign_spec.rb b/spec/minisign_spec.rb index f7b3739..2b029ea 100644 --- a/spec/minisign_spec.rb +++ b/spec/minisign_spec.rb @@ -28,10 +28,3 @@ end.to raise_error("Signature key id is 4CB7A94FABA329A6\nbut the key id in the public key is F15F69C58B18A08") end end - -describe Minisign::Signature do - it 'has a key id' do - @signature = Minisign::Signature.new(File.read('test/example.txt.minisig')) - expect(@signature.key_id).to eq('4CB7A94FABA329A6') - end -end diff --git a/spec/verify.sh b/spec/verify.sh new file mode 100755 index 0000000..85bc17d --- /dev/null +++ b/spec/verify.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +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 +fi diff --git a/test/generated/.keep b/test/generated/.keep new file mode 100644 index 0000000..e69de29