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..3e617fe 100644 --- a/lib/minisign.rb +++ b/lib/minisign.rb @@ -3,95 +3,5 @@ require 'ed25519' 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 'signature' +require 'public_key' diff --git a/lib/public_key.rb b/lib/public_key.rb new file mode 100644 index 0000000..39e7355 --- /dev/null +++ b/lib/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/signature.rb b/lib/signature.rb new file mode 100644 index 0000000..56745b4 --- /dev/null +++ b/lib/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..3c20870 100644 --- a/minisign.gemspec +++ b/minisign.gemspec @@ -12,6 +12,6 @@ Gem::Specification.new do |s| '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.7' s.metadata['rubygems_mfa_required'] = 'true' end