Skip to content

Commit

Permalink
parse private key (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
jshawl authored Feb 3, 2024
1 parent d479305 commit 7196f4a
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Verify key id match
- Parse private key
- Use ruby 2.7

## [0.0.5] - 2022-05-30

### Added
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
minisign (0.0.7)
ed25519 (~> 1.3)
rbnacl (~> 7.1)

GEM
remote: https://rubygems.org/
Expand All @@ -11,10 +12,13 @@ GEM
diff-lcs (1.5.0)
docile (1.4.0)
ed25519 (1.3.0)
ffi (1.16.3)
parallel (1.22.1)
parser (3.1.2.0)
ast (~> 2.4.1)
rainbow (3.1.1)
rbnacl (7.1.1)
ffi
regexp_parser (2.5.0)
rexml (3.2.5)
rspec (3.11.0)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Minisign

A rubygem for verifying [Minisign](http://jedisct1.github.io/minisign/) signatures.
A rubygem for creating and verifying [Minisign](http://jedisct1.github.io/minisign/) signatures.

## Installation & Usage

Expand Down
2 changes: 2 additions & 0 deletions lib/minisign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'ed25519'
require 'base64'
require 'openssl'
require 'rbnacl'

require 'minisign/public_key'
require 'minisign/signature'
require 'minisign/private_key'
40 changes: 40 additions & 0 deletions lib/minisign/private_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Minisign
# Parse ed25519 signing key from minisign private key
class PrivateKey
attr_reader :signature_algorithm, :kdf_algorithm, :cksum_algorithm, :kdf_salt, :kdf_opslimit, :kdf_memlimit,
:key_id, :public_key, :secret_key, :checksum

# rubocop:disable Metrics/AbcSize
def initialize(str, password = nil)
contents = str.split("\n")
bytes = Base64.decode64(contents.last).bytes
@signature_algorithm, @kdf_algorithm, @cksum_algorithm =
[bytes[0..1], bytes[2..3], bytes[4..5]].map { |a| a.pack('U*') }
@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
kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit)
@key_id, @secret_key, @public_key, @checksum = xor(kdf_output, bytes[54..157])
end
# rubocop:enable Metrics/AbcSize

def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
RbNaCl::PasswordHash.scrypt(
password,
kdf_salt.pack('C*'),
kdf_opslimit,
kdf_memlimit,
104
).bytes
end

def xor(kdf_output, contents)
xored = kdf_output.each_with_index.map do |b, i|
contents[i] ^ b
end
[xored[0..7], xored[8..39], xored[40..71], xored[72..103]]
end
end
end
1 change: 1 addition & 0 deletions minisign.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Gem::Specification.new do |s|
'https://rubygems.org/gems/minisign'
s.license = 'MIT'
s.add_runtime_dependency 'ed25519', '~> 1.3'
s.add_runtime_dependency 'rbnacl', '~> 7.1'
s.required_ruby_version = '>= 2.7'
s.metadata['rubygems_mfa_required'] = 'true'
end
53 changes: 53 additions & 0 deletions spec/minisign/private_key_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

describe Minisign::PrivateKey do
describe '.new' do
before(:all) do
@private_key = Minisign::PrivateKey.new(File.read('test/minisign.key'), 'password')
end

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

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

it 'parses the cksum_algorithm' do
expect(@private_key.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])
end

it 'parses the kdf_opslimit' do
expect(@private_key.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)
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
expect(@private_key.public_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]) # rubocop:disable Layout/LineLength
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
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])
end
end
end
10 changes: 5 additions & 5 deletions spec/minisign_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

describe Minisign::PublicKey do
before do
@pk = Minisign::PublicKey.new(File.read('test/local.pub').split("\n").pop)
@pk = Minisign::PublicKey.new(File.read('test/minisign.pub').split("\n").pop)
@message = File.read('test/example.txt')
end
it 'verifies signatures' do
@signature = Minisign::Signature.new(File.read('test/example.txt.minisig'))
expect(@pk.verify(@signature, @message)).to match('Trusted comment')
end
it 'raises ed25519 errors' do
it 'raises ed25519 errors for valid signatures but mismatching content' do
@signature = Minisign::Signature.new(File.read('test/example.txt.minisig.unverifiable'))
expect { @pk.verify(@signature, @message) }.to raise_error(Ed25519::VerifyError)
end
Expand All @@ -18,20 +18,20 @@
expect { @pk.verify(@signature, @message) }.to raise_error('Comment signature verification failed')
end
it 'has a key_id' do
expect(@pk.key_id).to eq('E86FECED695E8E0')
expect(@pk.key_id).to eq('4CB7A94FABA329A6')
end
it 'raises errors on key id mismatch' do
@pk = Minisign::PublicKey.new('RWQIoBiLxWlf8dGe/DM+igVgetlwOuhWW3abyI1z8eS1RHJVc4o+1sCI')
@signature = Minisign::Signature.new(File.read('test/example.txt.minisig'))
expect do
@pk.verify(@signature, @message)
end.to raise_error("Signature key id is E86FECED695E8E0\nbut the key id in the public key is F15F69C58B18A08")
end.to raise_error("Signature key id is 4CB7A94FABA329A6\nbut the key id in the public key is F15F69C58B18A08")
end
end

describe Minisign::Signature do
it 'has a key id' do
@signature = Minisign::Signature.new(File.read('test/example.txt.minisig'))
expect(@signature.key_id).to eq('E86FECED695E8E0')
expect(@signature.key_id).to eq('4CB7A94FABA329A6')
end
end
6 changes: 3 additions & 3 deletions test/example.txt.minisig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
untrusted comment: signature from minisign secret key
RUTg6JXWzv6GDmPMFIE8V3D+S6mi6FBFrUNwvRVZrLNrySSWT8HPLXhN16nSzb3WLTSd59SceVfGtgEP2FMHVAqTc40mLXBbJQ4=
trusted comment: timestamp:1653934067 file:example.txt hashed
YU7xAGNcc5LGLHHyw14S6xtIvhfF3chGJ/rLSauaiPb1jtnt6JHB/ieMIjqEZ8unxxLllXQ2t6uQqzIKsiwAAg==
RUSmKaOrT6m3TM3Pv4f4gwv40qbXWPv+l8LX1741TuRQV1gfnPWQqcRbEhRXzUxaAFTQocrOhC1SLRTV2kQZZSIi6nBK1FC9GgA=
trusted comment: timestamp:1706961104 file:example.txt hashed
LzQ3/VkobtbyEZX95vEJ5kf2brGY/yGpxqT3o1OU2Be4RnWXeRky3lHfwjFHGaLB+egSgrUfntjY3cgACiBxDg==
6 changes: 3 additions & 3 deletions test/example.txt.minisig.tampered
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
untrusted comment: signature from minisign secret key
RUTg6JXWzv6GDmPMFIE8V3D+S6mi6FBFrUNwvRVZrLNrySSWT8HPLXhN16nSzb3WLTSd59SceVfGtgEP2FMHVAqTc40mLXBbJQ4=
trusted comment: timestamp:1653934367 file:example.txt hashed
YU7xAGNcc5LGLHHyw14S6xtIvhfF3chGJ/rLSauaiPb1jtnt6JHB/ieMIjqEZ8unxxLllXQ2t6uQqzIKsiwAAg==
RUSmKaOrT6m3TM3Pv4f4gwv40qbXWPv+l8LX1741TuRQV1gfnPWQqcRbEhRXzUxaAFTQocrOhC1SLRTV2kQZZSIi6nBK1FC9GgA=
trusted comment: timestamp:1706961103 file:example.txt hashed
LzQ3/VkobtbyEZX95vEJ5kf2brGY/yGpxqT3o1OU2Be4RnWXeRky3lHfwjFHGaLB+egSgrUfntjY3cgACiBxDg==
6 changes: 3 additions & 3 deletions test/example.txt.minisig.unverifiable
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
untrusted comment: signature from minisign secret key
RUTg6JXWzv6GDmPMFIE8V3D+S5mi6FBFrUNwvRVZrLNrySSWT8HPLXhN16nSzb3WLTSd59SceVfGtgEP2FMHVAqTc40mLXBbJQ4=
trusted comment: timestamp:1653934067 file:example.txt hashed
YU7xAGNcc5LGLHHyw14S6xtIvhfF3chGJ/rLSauaiPb1jtnt6JHB/ieMIjqEZ8unxxLllXQ2t6uQqzIKsiwAAg==
RUSmKaOrT6m3TENGp8riRZz/zdCYneGKtF/1JiL23qh/WKfVKzQJhmu5/1IYSZzluvpBRmTm9TvVfvDjPTUHPp80Lrv3mwnB3wU=
trusted comment: timestamp:1706961511 file:example.txt.minisig.tampered hashed
nBME8TuE4sS+q4MGSSDxaacbX/dFTWRaixyKFTSlUNx9jydYgb5Ivg2JnzO0oO5NauHge8L5LG/5wa9RIJriDw==
2 changes: 0 additions & 2 deletions test/local.key

This file was deleted.

2 changes: 0 additions & 2 deletions test/local.pub

This file was deleted.

2 changes: 2 additions & 0 deletions test/minisign.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
untrusted comment: password is "password"
RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=
2 changes: 2 additions & 0 deletions test/minisign.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
untrusted comment: minisign public key 4CB7A94FABA329A6
RWSmKaOrT6m3TGwjwBovgOmlhSbyBUw3hyhnSOYruHXbJa36xHr8rq2M

0 comments on commit 7196f4a

Please sign in to comment.