From d479305ef31349893eb4db74c017ac2d5e0315be Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Wed, 31 Jan 2024 18:35:07 -0600 Subject: [PATCH] refactor public key (#3) --- .github/workflows/gem-push.yml | 4 +- .github/workflows/ruby.yml | 2 +- .rubocop.yml | 1 + lib/minisign.rb | 93 +--------------------------------- lib/minisign/public_key.rb | 50 ++++++++++++++++++ lib/minisign/signature.rb | 44 ++++++++++++++++ minisign.gemspec | 4 +- 7 files changed, 102 insertions(+), 96 deletions(-) create mode 100644 lib/minisign/public_key.rb create mode 100644 lib/minisign/signature.rb diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index 8a8a862..4da7b53 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -14,10 +14,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Ruby 2.6 + - name: Set up Ruby 2.7 uses: actions/setup-ruby@v1 with: - ruby-version: 2.6.x + ruby-version: 2.7.x - name: Publish to GPR run: | diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index bb07f24..c8c5fa7 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.6'] + ruby-version: ['2.7'] steps: - uses: actions/checkout@v3 diff --git a/.rubocop.yml b/.rubocop.yml index faa6d04..38ba3e2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,3 +2,4 @@ Metrics/BlockLength: IgnoredMethods: ['describe', 'context'] AllCops: NewCops: enable + TargetRubyVersion: 2.7.7 diff --git a/lib/minisign.rb b/lib/minisign.rb index ad4503e..b782534 100644 --- a/lib/minisign.rb +++ b/lib/minisign.rb @@ -4,94 +4,5 @@ require 'base64' require 'openssl' -# `minisign` is a rubygem for verifying {https://jedisct1.github.io/minisign minisign} signatures. -# @author Jesse Shawl -module Minisign - # Parse a .minisig file's contents - class Signature - # @param str [String] The contents of the .minisig file - # @example - # Minisign::Signature.new(File.read('test/example.txt.minisig')) - def initialize(str) - @lines = str.split("\n") - end - - # @return [String] the key id - # @example - # Minisign::Signature.new(File.read('test/example.txt.minisig')).key_id - # #=> "E86FECED695E8E0" - def key_id - encoded_signature[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase - end - - # @return [String] the trusted comment - # @example - # Minisign::Signature.new(File.read('test/example.txt.minisig')).trusted_comment - # #=> "timestamp:1653934067\tfile:example.txt\thashed" - def trusted_comment - @lines[2].split('trusted comment: ')[1] - end - - def trusted_comment_signature - Base64.decode64(@lines[3]) - end - - # @return [String] the signature - def signature - encoded_signature[10..] - end - - private - - def encoded_signature - Base64.decode64(@lines[1]) - end - end - - # Parse ed25519 verify key from minisign public key - class PublicKey - # Parse the ed25519 verify key from the minisign public key - # - # @param str [String] The minisign public key - # @example - # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM') - def initialize(str) - @decoded = Base64.strict_decode64(str) - @public_key = @decoded[10..] - @verify_key = Ed25519::VerifyKey.new(@public_key) - end - - # @return [String] the key id - # @example - # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM').key_id - # #=> "E86FECED695E8E0" - def key_id - @decoded[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase - end - - # Verify a message's signature - # - # @param sig [Minisign::Signature] - # @param message [String] the content that was signed - # @return [String] the trusted comment - # @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)) - begin - @verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment) - rescue Ed25519::VerifyError - raise 'Comment signature verification failed' - end - "Signature and comment signature verified\nTrusted comment: #{sig.trusted_comment}" - end - - private - - def ensure_matching_key_ids(key_id1, key_id2) - raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2 - end - end -end +require 'minisign/public_key' +require 'minisign/signature' diff --git a/lib/minisign/public_key.rb b/lib/minisign/public_key.rb new file mode 100644 index 0000000..39e7355 --- /dev/null +++ b/lib/minisign/public_key.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Minisign + # Parse ed25519 verify key from minisign public key + class PublicKey + # Parse the ed25519 verify key from the minisign public key + # + # @param str [String] The minisign public key + # @example + # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM') + def initialize(str) + @decoded = Base64.strict_decode64(str) + @public_key = @decoded[10..] + @verify_key = Ed25519::VerifyKey.new(@public_key) + end + + # @return [String] the key id + # @example + # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM').key_id + # #=> "E86FECED695E8E0" + def key_id + @decoded[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase + end + + # Verify a message's signature + # + # @param sig [Minisign::Signature] + # @param message [String] the content that was signed + # @return [String] the trusted comment + # @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)) + begin + @verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment) + rescue Ed25519::VerifyError + raise 'Comment signature verification failed' + end + "Signature and comment signature verified\nTrusted comment: #{sig.trusted_comment}" + end + + private + + def ensure_matching_key_ids(key_id1, key_id2) + raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2 + end + end +end diff --git a/lib/minisign/signature.rb b/lib/minisign/signature.rb new file mode 100644 index 0000000..56745b4 --- /dev/null +++ b/lib/minisign/signature.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Minisign + # Parse a .minisig file's contents + class Signature + # @param str [String] The contents of the .minisig file + # @example + # Minisign::Signature.new(File.read('test/example.txt.minisig')) + def initialize(str) + @lines = str.split("\n") + end + + # @return [String] the key id + # @example + # Minisign::Signature.new(File.read('test/example.txt.minisig')).key_id + # #=> "E86FECED695E8E0" + def key_id + encoded_signature[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase + end + + # @return [String] the trusted comment + # @example + # Minisign::Signature.new(File.read('test/example.txt.minisig')).trusted_comment + # #=> "timestamp:1653934067\tfile:example.txt\thashed" + def trusted_comment + @lines[2].split('trusted comment: ')[1] + end + + def trusted_comment_signature + Base64.decode64(@lines[3]) + end + + # @return [String] the signature + def signature + encoded_signature[10..] + end + + private + + def encoded_signature + Base64.decode64(@lines[1]) + end + end +end diff --git a/minisign.gemspec b/minisign.gemspec index 21de057..91506ff 100644 --- a/minisign.gemspec +++ b/minisign.gemspec @@ -7,11 +7,11 @@ Gem::Specification.new do |s| s.description = 'Verify minisign signatures' s.authors = ['Jesse Shawl'] s.email = 'jesse@jesse.sh' - s.files = ['lib/minisign.rb'] + s.files = Dir['lib/**/*'] s.homepage = 'https://rubygems.org/gems/minisign' s.license = 'MIT' s.add_runtime_dependency 'ed25519', '~> 1.3' - s.required_ruby_version = '>= 2.6.0' + s.required_ruby_version = '>= 2.7' s.metadata['rubygems_mfa_required'] = 'true' end