Skip to content

Commit

Permalink
Refactor public key (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
jshawl authored Feb 10, 2024
1 parent d56d926 commit 20b0eda
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 76 deletions.
7 changes: 3 additions & 4 deletions lib/minisign/key_pair.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ def initialize(password = nil)
@password = password
@key_id = SecureRandom.bytes(8)
@signing_key = Ed25519::SigningKey.generate
kd = key_data

@checksum = blake2b256("Ed#{kd}")
@keynum_sk = "#{kd}#{@checksum}"
@checksum = blake2b256("Ed#{key_data}")
@keynum_sk = "#{key_data}#{@checksum}"

@kdf_salt = SecureRandom.bytes(32)
@keynum_sk = xor(kdf_output, @keynum_sk.bytes).pack('C*') if @password
Expand Down Expand Up @@ -48,7 +47,7 @@ def kdf_output
end

def key_data
"#{@key_id}#{@signing_key.to_bytes}#{@signing_key.verify_key.to_bytes}"
@key_data ||= "#{@key_id}#{@signing_key.to_bytes}#{@signing_key.verify_key.to_bytes}"
end

# 🤷
Expand Down
45 changes: 21 additions & 24 deletions lib/minisign/private_key.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
# frozen_string_literal: true

module Minisign
# Parse ed25519 signing key from minisign private key
# The private key used to create signatures
class PrivateKey
include Utils
attr_reader :kdf_salt, :kdf_opslimit, :kdf_memlimit,
:key_id, :ed25519_public_key_bytes, :ed25519_private_key_bytes, :checksum

# rubocop:disable Layout/LineLength
attr_reader :key_id

# Parse signing information from the minisign private key
#
# @param str [String] The minisign private key
# @param password [String] The password used to encrypt the private key
# @example
# Minisign::PrivateKey.new(
# 'RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=',
# 'password'
# )
# Minisign::PrivateKey.new(File.read("test/minisign.key"), 'password')
def initialize(str, password = nil)
comment, data = str.split("\n")
@password = password
Expand All @@ -25,22 +20,12 @@ def initialize(str, password = nil)
@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

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*')
assert_valid_key!
end

# Get the corresponding public key from the private key
#
# @return [Minisign::PublicKey]
def public_key
data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key_bytes.pack('C*')}")
Minisign::PublicKey.new(data)
Expand Down Expand Up @@ -78,12 +63,24 @@ def to_s

private

def signature_algorithm
@bytes[0..1].pack('U*')
end

def cksum_algorithm
@bytes[4..5].pack('U*')
end

def kdf_algorithm
@bytes[2..3].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!
def assert_valid_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
Expand Down
67 changes: 42 additions & 25 deletions lib/minisign/public_key.rb
Original file line number Diff line number Diff line change
@@ -1,63 +1,80 @@
# frozen_string_literal: true

module Minisign
# Parse ed25519 verify key from minisign public key
# The public key used to verify signatures
class PublicKey
include Utils
# Parse the ed25519 verify key from the minisign public key
# Read a minisign public key
#
# @param str [String] The minisign public key
# @example
# Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM')
# # or from a file:
# Minisign::PublicKey.new(File.read('test/minisign.pub'))
def initialize(str)
parts = str.split("\n")
@decoded = Base64.strict_decode64(parts.last)
@public_key = @decoded[10..]
@verify_key = Ed25519::VerifyKey.new(@public_key)
@untrusted_comment = if parts.length == 1
"minisign public key #{key_id}"
else
parts.first.split('untrusted comment: ').last
end
@lines = str.split("\n")
@decoded = Base64.strict_decode64(@lines.last)
end

# @return [String] the key id
# @example
# Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM').key_id
# public_key.key_id
# #=> "E86FECED695E8E0"
def key_id
@decoded[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
key_id_binary_string.bytes.map { |c| c.to_s(16) }.reverse.join.upcase
end

# Verify a message's signature
#
# @param sig [Minisign::Signature]
# @param signature [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)
ensure_matching_key_ids(sig.key_id, key_id)
@verify_key.verify(sig.signature, blake2b512(message))
# @raise RuntimeError on mismatching key ids
def verify(signature, message)
assert_matching_key_ids!(signature.key_id, key_id)
ed25519_verify_key.verify(signature.signature, blake2b512(message))
begin
@verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment)
ed25519_verify_key.verify(signature.trusted_comment_signature, signature.signature + signature.trusted_comment)
rescue Ed25519::VerifyError
raise 'Comment signature verification failed'
end
"Signature and comment signature verified\nTrusted comment: #{sig.trusted_comment}"
end

def key_data
Base64.strict_encode64("Ed#{@decoded[2..9]}#{@public_key}")
"Signature and comment signature verified\nTrusted comment: #{signature.trusted_comment}"
end

# @return [String] The public key that can be written to a file
def to_s
"untrusted comment: #{@untrusted_comment}\n#{key_data}\n"
"untrusted comment: #{untrusted_comment}\n#{key_data}\n"
end

private

def ensure_matching_key_ids(key_id1, key_id2)
def untrusted_comment
if @lines.length == 1
"minisign public key #{key_id}"
else
@lines.first.split('untrusted comment: ').last
end
end

def key_id_binary_string
@decoded[2..9]
end

def ed25519_public_key_binary_string
@decoded[10..]
end

def ed25519_verify_key
Ed25519::VerifyKey.new(ed25519_public_key_binary_string)
end

def key_data
Base64.strict_encode64("Ed#{key_id_binary_string}#{ed25519_public_key_binary_string}")
end

def assert_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
Expand Down
4 changes: 3 additions & 1 deletion lib/minisign/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ def trusted_comment
@lines[2].split('trusted comment: ')[1]
end

# @return [String] the signature for the trusted comment
def trusted_comment_signature
Base64.decode64(@lines[3])
end

# @return [String] the signature
# @return [String] the global signature
def signature
encoded_signature[10..]
end

# @return [String] The signature that can be written to a file
def to_s
"#{@lines.join("\n")}\n"
end
Expand Down
36 changes: 14 additions & 22 deletions spec/minisign/private_key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@

describe '.new' do
it 'parses the signature_algorithm' do
expect(@private_key.signature_algorithm).to eq('Ed')
expect(@private_key.send(:signature_algorithm)).to eq('Ed')
end

it 'parses the kdf_algorithm' do
expect(@private_key.kdf_algorithm).to eq('Sc')
expect(@private_key.send(:kdf_algorithm)).to eq('Sc')
end

it 'parses the kdf_algorithm' do
@unencrypted_private_key = Minisign::PrivateKey.new(File.read('test/unencrypted.key'))
expect(@unencrypted_private_key.kdf_algorithm.unpack('C*')).to eq([0, 0])
expect(@unencrypted_private_key.send(:kdf_algorithm).unpack('C*')).to eq([0, 0])
end

it 'raises if the private key requires a password but is not supplied' do
Expand All @@ -32,51 +32,43 @@
end

it 'parses the cksum_algorithm' do
expect(@private_key.cksum_algorithm).to eq('B2')
expect(@private_key.send(:cksum_algorithm)).to eq('B2')
end

it 'parses the kdf_salt' do
expect(@private_key.kdf_salt).to eq([17, 255, 178, 97, 174, 94, 1, 125, 252, 62, 7, 107, 35, 116, 204, 199, 12,
190, 222, 200, 51, 166, 7, 25, 89, 5, 225, 56, 170, 157, 127, 219])
kdf_salt = @private_key.instance_variable_get('@kdf_salt')
expect(kdf_salt).to eq([17, 255, 178, 97, 174, 94, 1, 125, 252, 62, 7, 107, 35, 116, 204, 199, 12,
190, 222, 200, 51, 166, 7, 25, 89, 5, 225, 56, 170, 157, 127, 219])
end

it 'parses the kdf_opslimit' do
expect(@private_key.kdf_opslimit).to eq(33_554_432)
expect(@private_key.instance_variable_get('@kdf_opslimit')).to eq(33_554_432)
end

it 'parses the kdf_memlimit' do
expect(@private_key.kdf_memlimit).to eq(1_073_741_824)
expect(@private_key.instance_variable_get('@kdf_memlimit')).to eq(1_073_741_824)
end

it 'parses the key id' do
expect(@private_key.key_id).to eq([166, 41, 163, 171, 79, 169, 183, 76])
end

it 'parses the public key' do
key = @private_key.ed25519_public_key_bytes
key = @private_key.instance_variable_get('@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
key = @private_key.ed25519_private_key_bytes
key = @private_key.instance_variable_get('@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
expect(@private_key.checksum).to eq([19, 146, 239, 121, 33, 164, 216, 219, 8, 104, 111, 52, 198, 78, 21, 236,
113, 255, 174, 47, 39, 216, 61, 198, 233, 161, 233, 143, 84, 246, 255, 150])

key_data = [
[69, 100],
@private_key.key_id,
@private_key.ed25519_private_key_bytes,
@private_key.ed25519_public_key_bytes
].inject(&:+).pack('C*')

computed_checksum = blake2b256(key_data).bytes
expect(@private_key.checksum).to eq(computed_checksum)
checksum = @private_key.instance_variable_get('@checksum')
expect(checksum).to eq([19, 146, 239, 121, 33, 164, 216, 219, 8, 104, 111, 52, 198, 78, 21, 236,
113, 255, 174, 47, 39, 216, 61, 198, 233, 161, 233, 143, 84, 246, 255, 150])
end

it 'can be written to a file' do
Expand Down

0 comments on commit 20b0eda

Please sign in to comment.