diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..faa6d04 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,4 @@ +Metrics/BlockLength: + IgnoredMethods: ['describe', 'context'] +AllCops: + NewCops: enable diff --git a/README.md b/README.md index 1e08772..ccc08e1 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,20 @@ A rubygem for verifying [Minisign](http://jedisct1.github.io/minisign/) signatur gem install minisign ``` +### Verify a signature + ```rb require 'minisign' -pk = Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM') -signature = Minisign::Signature.new(File.read("test/example.txt.minisig")) +public_key = Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM') message = File.read("test/example.txt") -pk.verify(signature, message) +signature = Minisign::Signature.new(File.read("test/example.txt.minisig")) +public_key.verify(signature, message) +``` + +The above is equivalent to: + +``` +minisign -Vm test/example.txt -P RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM ``` ## Local Development @@ -21,3 +29,9 @@ pk.verify(signature, message) ``` irb -Ilib -rminisign ``` + +## Local Documentation + +``` +yard server --reload +``` diff --git a/lib/minisign.rb b/lib/minisign.rb index 27e2c47..ad4503e 100644 --- a/lib/minisign.rb +++ b/lib/minisign.rb @@ -9,23 +9,42 @@ module Minisign # Parse a .minisig file's contents class Signature - attr_reader :signature, :comment, :comment_signature - - # @!attribute [r] signature - # @return [String] the ed25519 verify key - # @!attribute [r] comment_signature - # @return [String] the signature for the trusted comment - # @!attribute [r] comment - # @return [String] the trusted comment - # @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") - @signature = Base64.decode64(lines[1])[10..] - @comment = lines[2].split('trusted comment: ')[1] - @comment_signature = Base64.decode64(lines[3]) + @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 @@ -37,10 +56,19 @@ class PublicKey # @example # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM') def initialize(str) - @public_key = Base64.strict_decode64(str)[10..] + @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] @@ -50,13 +78,20 @@ def initialize(str) # @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.comment_signature, sig.signature + sig.comment) + @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.comment}" + "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/minisign.gemspec b/minisign.gemspec index 1997fd4..34e2dd9 100644 --- a/minisign.gemspec +++ b/minisign.gemspec @@ -13,4 +13,5 @@ Gem::Specification.new do |s| s.license = 'MIT' s.add_runtime_dependency 'ed25519', '~> 1.3' s.required_ruby_version = '>= 2.6.0' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/spec/minisign_spec.rb b/spec/minisign_spec.rb index 6e25693..05d4559 100644 --- a/spec/minisign_spec.rb +++ b/spec/minisign_spec.rb @@ -17,4 +17,21 @@ @signature = Minisign::Signature.new(File.read('test/example.txt.minisig.tampered')) expect { @pk.verify(@signature, @message) }.to raise_error('Comment signature verification failed') end + it 'has a key_id' do + expect(@pk.key_id).to eq('E86FECED695E8E0') + end + it 'raises errors on key id mismatch' do + @pk = Minisign::PublicKey.new('RWQIoBiLxWlf8dGe/DM+igVgetlwOuhWW3abyI1z8eS1RHJVc4o+1sCI') + @signature = Minisign::Signature.new(File.read('test/example.txt.minisig')) + expect do + @pk.verify(@signature, @message) + end.to raise_error("Signature key id is E86FECED695E8E0\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('E86FECED695E8E0') + end end