From abb8178837e4ccab8975bb17a076a8c67a654f1a Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 06:28:23 -0600 Subject: [PATCH 1/7] wip --- lib/minisign/private_key.rb | 44 +++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index 99099ba..f62bbc7 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -7,9 +7,7 @@ class PrivateKey attr_reader :signature_algorithm, :kdf_algorithm, :cksum_algorithm, :kdf_salt, :kdf_opslimit, :kdf_memlimit, :key_id, :ed25519_public_key, :secret_key, :checksum - # rubocop:disable Metrics/AbcSize # rubocop:disable Layout/LineLength - # rubocop:disable Metrics/MethodLength # Parse signing information from the minisign private key # @@ -17,37 +15,34 @@ class PrivateKey # @example # Minisign::PrivateKey.new('RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', 'password') def initialize(str, password = nil) - contents = str.split("\n") - decoded = Base64.decode64(contents.last) - @untrusted_comment = contents.first.split('untrusted comment: ').last + comment, data = str.split("\n") + @password = password + decoded = Base64.decode64(data) + @untrusted_comment = comment.split('untrusted comment: ').last bytes = decoded.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 - @keynum_sk = bytes[54..157].pack('C*') - @key_data_bytes = if password - kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit) - xor(kdf_output, bytes[54..157]) - else - bytes[54..157] - end - @key_id, @secret_key, @ed25519_public_key, @checksum = key_data(@key_data_bytes) - assert_keypair_match! + @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(bytes) + @key_id, @secret_key, @ed25519_public_key, @checksum = key_data(password, bytes[54..157]) + validate_key! end # rubocop:enable Layout/LineLength - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength + + def scrypt_params(bytes) + [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum] + end # @raise [RuntimeError] if the extracted public key does not match the derived public key - def assert_keypair_match! + def validate_key! + raise 'Missing password for encrypted key' if @kdf_algorithm.bytes.sum != 0 && @password.nil? raise 'Wrong password for that key' if @ed25519_public_key != ed25519_signing_key.verify_key.to_bytes.bytes end - def key_data(bytes) + def key_data(password, bytes) + if password + kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit) + bytes = xor(kdf_output, bytes) + end [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] end @@ -85,7 +80,8 @@ def to_s kdf_salt = @kdf_salt.pack('C*') kdf_opslimit = [@kdf_opslimit, 0].pack('L*') kdf_memlimit = [@kdf_memlimit, 0].pack('L*') - data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{@keynum_sk}" + keynum_sk = key_data(@password, @key_id + @secret_key + @ed25519_public_key + @checksum) + data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.flatten.pack('C*')}" "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n" end end From 2d83ef4cc72c268826422799281da58dbb2635ea Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 06:36:27 -0600 Subject: [PATCH 2/7] refactor private key --- lib/minisign/private_key.rb | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index f62bbc7..5539417 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -4,7 +4,7 @@ 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, + attr_reader :kdf_salt, :kdf_opslimit, :kdf_memlimit, :key_id, :ed25519_public_key, :secret_key, :checksum # rubocop:disable Layout/LineLength @@ -13,28 +13,41 @@ class PrivateKey # # @param str [String] The minisign private key # @example - # Minisign::PrivateKey.new('RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', 'password') + # Minisign::PrivateKey.new( + # 'RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', + # 'password' + # ) def initialize(str, password = nil) comment, data = str.split("\n") @password = password decoded = Base64.decode64(data) @untrusted_comment = comment.split('untrusted comment: ').last - bytes = decoded.bytes - @signature_algorithm, @kdf_algorithm, @cksum_algorithm = - [bytes[0..1], bytes[2..3], bytes[4..5]].map { |a| a.pack('U*') } - @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(bytes) - @key_id, @secret_key, @ed25519_public_key, @checksum = key_data(password, bytes[54..157]) + @bytes = decoded.bytes + @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes) + @key_id, @secret_key, @ed25519_public_key, @checksum = key_data(password, @bytes[54..157]) validate_key! end # rubocop:enable Layout/LineLength + def signature_algorithm + @bytes[0..1].pack('U*') + end + + def kdf_algorithm + @bytes[2..3].pack('U*') + end + + def cksum_algorithm + @bytes[4..5].pack('U*') + end + def scrypt_params(bytes) [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum] end # @raise [RuntimeError] if the extracted public key does not match the derived public key def validate_key! - raise 'Missing password for encrypted key' if @kdf_algorithm.bytes.sum != 0 && @password.nil? + raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil? raise 'Wrong password for that key' if @ed25519_public_key != ed25519_signing_key.verify_key.to_bytes.bytes end From cbad40039e0783a14d52f704edb4afc935413f1a Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 06:46:55 -0600 Subject: [PATCH 3/7] refactor --- .rspec | 1 + lib/minisign/private_key.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.rspec b/.rspec index c99d2e7..5be63fc 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ --require spec_helper +--format documentation diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index 5539417..b782f11 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -65,7 +65,7 @@ def ed25519_signing_key end def public_key - data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{ed25519_signing_key.verify_key.to_bytes}") + data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key.pack('C*')}") Minisign::PublicKey.new(data) end From 4d904b4adeb0fce0520bb9d22979b686d0447d95 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 06:49:47 -0600 Subject: [PATCH 4/7] add type suffix --- lib/minisign/private_key.rb | 10 +++++----- spec/minisign/private_key_spec.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index b782f11..044fb79 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -5,7 +5,7 @@ module Minisign class PrivateKey include Utils attr_reader :kdf_salt, :kdf_opslimit, :kdf_memlimit, - :key_id, :ed25519_public_key, :secret_key, :checksum + :key_id, :ed25519_public_key_bytes, :secret_key, :checksum # rubocop:disable Layout/LineLength @@ -24,7 +24,7 @@ def initialize(str, password = nil) @untrusted_comment = comment.split('untrusted comment: ').last @bytes = decoded.bytes @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes) - @key_id, @secret_key, @ed25519_public_key, @checksum = key_data(password, @bytes[54..157]) + @key_id, @secret_key, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157]) validate_key! end # rubocop:enable Layout/LineLength @@ -48,7 +48,7 @@ def scrypt_params(bytes) # @raise [RuntimeError] if the extracted public key does not match the derived public key def validate_key! raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil? - raise 'Wrong password for that key' if @ed25519_public_key != ed25519_signing_key.verify_key.to_bytes.bytes + raise 'Wrong password for that key' if @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes end def key_data(password, bytes) @@ -65,7 +65,7 @@ def ed25519_signing_key end def public_key - data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key.pack('C*')}") + data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key_bytes.pack('C*')}") Minisign::PublicKey.new(data) end @@ -93,7 +93,7 @@ def to_s kdf_salt = @kdf_salt.pack('C*') kdf_opslimit = [@kdf_opslimit, 0].pack('L*') kdf_memlimit = [@kdf_memlimit, 0].pack('L*') - keynum_sk = key_data(@password, @key_id + @secret_key + @ed25519_public_key + @checksum) + keynum_sk = key_data(@password, @key_id + @secret_key + @ed25519_public_key_bytes + @checksum) data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.flatten.pack('C*')}" "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n" end diff --git a/spec/minisign/private_key_spec.rb b/spec/minisign/private_key_spec.rb index d689960..e58aac0 100644 --- a/spec/minisign/private_key_spec.rb +++ b/spec/minisign/private_key_spec.rb @@ -53,7 +53,7 @@ end it 'parses the public key' do - key = @private_key.ed25519_public_key + key = @private_key.ed25519_public_key_bytes expect(key).to eq([108, 35, 192, 26, 47, 128, 233, 165, 133, 38, 242, 5, 76, 55, 135, 40, 103, 72, 230, 43, 184, 117, 219, 37, 173, 250, 196, 122, 252, 174, 173, 140]) end @@ -71,7 +71,7 @@ [69, 100], @private_key.key_id, @private_key.secret_key, - @private_key.ed25519_public_key + @private_key.ed25519_public_key_bytes ].inject(&:+).pack('C*') computed_checksum = blake2b256(key_data).bytes From b4356a755e5c49157f6c6c6fbd7c12a08ac5977d Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 06:52:02 -0600 Subject: [PATCH 5/7] s/secret_key/ed25519_private_key_bytes --- lib/minisign/private_key.rb | 8 ++++---- spec/minisign/private_key_spec.rb | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index 044fb79..aa76ace 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -5,7 +5,7 @@ module Minisign class PrivateKey include Utils attr_reader :kdf_salt, :kdf_opslimit, :kdf_memlimit, - :key_id, :ed25519_public_key_bytes, :secret_key, :checksum + :key_id, :ed25519_public_key_bytes, :ed25519_private_key_bytes, :checksum # rubocop:disable Layout/LineLength @@ -24,7 +24,7 @@ def initialize(str, password = nil) @untrusted_comment = comment.split('untrusted comment: ').last @bytes = decoded.bytes @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes) - @key_id, @secret_key, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157]) + @key_id, @ed25519_private_key_bytes, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157]) validate_key! end # rubocop:enable Layout/LineLength @@ -61,7 +61,7 @@ def key_data(password, bytes) # @return [Ed25519::SigningKey] the ed25519 signing key def ed25519_signing_key - Ed25519::SigningKey.new(@secret_key.pack('C*')) + Ed25519::SigningKey.new(@ed25519_private_key_bytes.pack('C*')) end def public_key @@ -93,7 +93,7 @@ def to_s kdf_salt = @kdf_salt.pack('C*') kdf_opslimit = [@kdf_opslimit, 0].pack('L*') kdf_memlimit = [@kdf_memlimit, 0].pack('L*') - keynum_sk = key_data(@password, @key_id + @secret_key + @ed25519_public_key_bytes + @checksum) + keynum_sk = key_data(@password, @key_id + @ed25519_private_key_bytes + @ed25519_public_key_bytes + @checksum) data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.flatten.pack('C*')}" "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n" end diff --git a/spec/minisign/private_key_spec.rb b/spec/minisign/private_key_spec.rb index e58aac0..7613d5e 100644 --- a/spec/minisign/private_key_spec.rb +++ b/spec/minisign/private_key_spec.rb @@ -59,8 +59,9 @@ end it 'parses the secret key' do - expect(@private_key.secret_key).to eq([65, 87, 110, 33, 168, 130, 118, 100, 249, 200, 160, 167, 47, 59, 141, - 122, 156, 38, 80, 199, 139, 1, 21, 18, 116, 110, 204, 131, 199, 202, 181, 87]) # rubocop:disable Layout/LineLength + key = @private_key.ed25519_private_key_bytes + expect(key).to eq([65, 87, 110, 33, 168, 130, 118, 100, 249, 200, 160, 167, 47, 59, 141, + 122, 156, 38, 80, 199, 139, 1, 21, 18, 116, 110, 204, 131, 199, 202, 181, 87]) end it 'parses the checksum' do @@ -70,7 +71,7 @@ key_data = [ [69, 100], @private_key.key_id, - @private_key.secret_key, + @private_key.ed25519_private_key_bytes, @private_key.ed25519_public_key_bytes ].inject(&:+).pack('C*') From 8b73e388cb4b723347a505aa521ed9507ea99c4b Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 06:55:18 -0600 Subject: [PATCH 6/7] clarify flatten --- lib/minisign/private_key.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index aa76ace..cab277c 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -93,8 +93,9 @@ def to_s kdf_salt = @kdf_salt.pack('C*') kdf_opslimit = [@kdf_opslimit, 0].pack('L*') kdf_memlimit = [@kdf_memlimit, 0].pack('L*') - keynum_sk = key_data(@password, @key_id + @ed25519_private_key_bytes + @ed25519_public_key_bytes + @checksum) - data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.flatten.pack('C*')}" + keynum_sk = key_data(@password, + @key_id + @ed25519_private_key_bytes + @ed25519_public_key_bytes + @checksum).flatten + data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.pack('C*')}" "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n" end end From 863e1e39a4d8cde5fe55ff8a6b0e6f5e30e3395b Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 9 Feb 2024 07:08:10 -0600 Subject: [PATCH 7/7] make internal methods private --- .rubocop.yml | 3 ++- lib/minisign/private_key.rb | 48 +++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 38ba3e2..6a4ec2e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,6 @@ Metrics/BlockLength: - IgnoredMethods: ['describe', 'context'] + AllowedMethods: ['describe', 'context'] AllCops: NewCops: enable TargetRubyVersion: 2.7.7 + SuggestExtensions: false diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index cab277c..048886e 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -41,29 +41,6 @@ def cksum_algorithm @bytes[4..5].pack('U*') end - def scrypt_params(bytes) - [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum] - end - - # @raise [RuntimeError] if the extracted public key does not match the derived public key - def validate_key! - raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil? - raise 'Wrong password for that key' if @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes - end - - def key_data(password, bytes) - if password - kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit) - bytes = xor(kdf_output, bytes) - end - [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] - end - - # @return [Ed25519::SigningKey] the ed25519 signing key - def ed25519_signing_key - Ed25519::SigningKey.new(@ed25519_private_key_bytes.pack('C*')) - end - def public_key data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key_bytes.pack('C*')}") Minisign::PublicKey.new(data) @@ -98,5 +75,30 @@ def to_s data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.pack('C*')}" "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n" end + + private + + def scrypt_params(bytes) + [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum] + end + + # @raise [RuntimeError] if the extracted public key does not match the derived public key + def validate_key! + raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil? + raise 'Wrong password for that key' if @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes + end + + def key_data(password, bytes) + if password + kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit) + bytes = xor(kdf_output, bytes) + end + [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] + end + + # @return [Ed25519::SigningKey] the ed25519 signing key + def ed25519_signing_key + Ed25519::SigningKey.new(@ed25519_private_key_bytes.pack('C*')) + end end end