Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor private key #19

Merged
merged 7 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading