Skip to content

Commit

Permalink
Refactor private key (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
jshawl authored Feb 9, 2024
1 parent e1aee2d commit d56d926
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 43 deletions.
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
--require spec_helper
--format documentation
3 changes: 2 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Metrics/BlockLength:
IgnoredMethods: ['describe', 'context']
AllowedMethods: ['describe', 'context']
AllCops:
NewCops: enable
TargetRubyVersion: 2.7.7
SuggestExtensions: false
86 changes: 49 additions & 37 deletions lib/minisign/private_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,45 @@ 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, :ed25519_public_key, :secret_key, :checksum
attr_reader :kdf_salt, :kdf_opslimit, :kdf_memlimit,
:key_id, :ed25519_public_key_bytes, :ed25519_private_key_bytes, :checksum

# rubocop:disable Metrics/AbcSize
# rubocop:disable Layout/LineLength
# rubocop:disable Metrics/MethodLength

# 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')
# 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
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!
comment, data = str.split("\n")
@password = password
decoded = Base64.decode64(data)
@untrusted_comment = comment.split('untrusted comment: ').last
@bytes = decoded.bytes
@kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes)
@key_id, @ed25519_private_key_bytes, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157])
validate_key!
end
# rubocop:enable Layout/LineLength
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength

# @raise [RuntimeError] if the extracted public key does not match the derived public key
def assert_keypair_match!
raise 'Wrong password for that key' if @ed25519_public_key != ed25519_signing_key.verify_key.to_bytes.bytes
def signature_algorithm
@bytes[0..1].pack('U*')
end

def key_data(bytes)
[bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]]
def kdf_algorithm
@bytes[2..3].pack('U*')
end

# @return [Ed25519::SigningKey] the ed25519 signing key
def ed25519_signing_key
Ed25519::SigningKey.new(@secret_key.pack('C*'))
def cksum_algorithm
@bytes[4..5].pack('U*')
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_bytes.pack('C*')}")
Minisign::PublicKey.new(data)
end

Expand Down Expand Up @@ -85,8 +70,35 @@ 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 + @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

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
11 changes: 6 additions & 5 deletions spec/minisign/private_key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@
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

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
Expand All @@ -70,8 +71,8 @@
key_data = [
[69, 100],
@private_key.key_id,
@private_key.secret_key,
@private_key.ed25519_public_key
@private_key.ed25519_private_key_bytes,
@private_key.ed25519_public_key_bytes
].inject(&:+).pack('C*')

computed_checksum = blake2b256(key_data).bytes
Expand Down

0 comments on commit d56d926

Please sign in to comment.