Skip to content

Commit

Permalink
create signature (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
jshawl authored Feb 3, 2024
1 parent 7196f4a commit 3397ada
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ jobs:
run: bundle exec rubocop
- name: Run tests
run: bundle exec rspec
- name: Test against jedisct1/minisign
run: ./spec/verify.sh
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
coverage
doc
.yardoc
test/generated/*
!test/generated/.keep
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

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

## [0.0.7] - 2022-06-22

### Changed
- Update bundler version

## [0.0.6] - 2022-06-22

### Added
- Verify key id match

## [0.0.5] - 2022-05-30

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ The above is equivalent to:
minisign -Vm test/example.txt -P RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM
```

### Create a signature

```rb
require 'minisign'
file_path = "example.txt"
password = "password"
private_key = Minisign::PrivateKey.new(File.read("minisign.key"), password)
signature = private_key.sign(file_path, File.read(file_path))

File.write("#{file_path}.minisig", signature.to_s)
```

## Local Development

```
Expand Down
1 change: 1 addition & 0 deletions lib/minisign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'openssl'
require 'rbnacl'

require 'minisign/utils'
require 'minisign/public_key'
require 'minisign/signature'
require 'minisign/private_key'
33 changes: 33 additions & 0 deletions lib/minisign/private_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
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, :public_key, :secret_key, :checksum

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

# 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')
def initialize(str, password = nil)
contents = str.split("\n")
bytes = Base64.decode64(contents.last).bytes
Expand All @@ -18,8 +26,10 @@ def initialize(str, password = nil)
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 Layout/LineLength
# rubocop:enable Metrics/AbcSize

# @return [String] the <kdf_output> used to xor the ed25519 keys
def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
RbNaCl::PasswordHash.scrypt(
password,
Expand All @@ -30,11 +40,34 @@ def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
).bytes
end

# rubocop:disable Layout/LineLength

# @return [Array<32 bit unsigned ints>] the byte array containing the key id, the secret and public ed25519 keys, and the checksum
def xor(kdf_output, contents)
# rubocop:enable Layout/LineLength
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

# @return [Ed25519::SigningKey] the ed25519 signing key
def ed25519_signing_key
Ed25519::SigningKey.new(@secret_key.pack('C*'))
end

# @return [String] the signature in the .minisig format that can be written to a file.
def sign(filename, message)
signature = ed25519_signing_key.sign(blake2b512(message))
trusted_comment = "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}")
[
'untrusted comment: <arbitrary text>',
Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"),
"trusted comment: #{trusted_comment}",
Base64.strict_encode64(global_signature),
''
].join("\n")
end
end
end
4 changes: 2 additions & 2 deletions lib/minisign/public_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Minisign
# Parse ed25519 verify key from minisign public key
class PublicKey
include Utils
# Parse the ed25519 verify key from the minisign public key
#
# @param str [String] The minisign public key
Expand Down Expand Up @@ -30,9 +31,8 @@ def key_id
# @raise Ed25519::VerifyError on invalid signatures
# @raise RuntimeError on tampered trusted comments
def verify(sig, message)
blake = OpenSSL::Digest.new('BLAKE2b512')
ensure_matching_key_ids(sig.key_id, key_id)
@verify_key.verify(sig.signature, blake.digest(message))
@verify_key.verify(sig.signature, blake2b512(message))
begin
@verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment)
rescue Ed25519::VerifyError
Expand Down
10 changes: 10 additions & 0 deletions lib/minisign/utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module Minisign
# Helpers used in multiple classes
module Utils
def blake2b512(message)
OpenSSL::Digest.new('BLAKE2b512').digest(message)
end
end
end
22 changes: 18 additions & 4 deletions spec/minisign/private_key_spec.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# 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
before(:all) do
@private_key = Minisign::PrivateKey.new(File.read('test/minisign.key'), 'password')
end

describe '.new' do
it 'parses the signature_algorithm' do
expect(@private_key.signature_algorithm).to eq('Ed')
end
Expand Down Expand Up @@ -50,4 +50,18 @@
113, 255, 174, 47, 39, 216, 61, 198, 233, 161, 233, 143, 84, 246, 255, 150])
end
end

describe 'sign' do
it 'signs a file' do
Dir.glob('test/generated/*').each { |file| File.delete(file) }
filename = "#{SecureRandom.uuid}.txt"
message = SecureRandom.uuid
File.write("test/generated/#{filename}", message)
signature = @private_key.sign(filename, message)
File.write("test/generated/#{filename}.minisig", signature)
@signature = Minisign::Signature.new(signature)
@public_key = Minisign::PublicKey.new('RWSmKaOrT6m3TGwjwBovgOmlhSbyBUw3hyhnSOYruHXbJa36xHr8rq2M')
expect(@public_key.verify(@signature, message)).to match('Signature and comment signature verified')
end
end
end
8 changes: 8 additions & 0 deletions spec/minisign/signature_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

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('4CB7A94FABA329A6')
end
end
7 changes: 0 additions & 7 deletions spec/minisign_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,3 @@
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('4CB7A94FABA329A6')
end
end
13 changes: 13 additions & 0 deletions spec/verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

if [[ "$OSTYPE" == "darwin"* ]]; then
url="https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-macos.zip"
curl -sL $url -o test/generated/minisign.zip
unzip -o test/generated/minisign.zip -d test/generated
test/generated/minisign -Vm test/generated/*.txt -p test/minisign.pub
else
url="https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz"
curl -sL $url -o test/generated/minisign.tar.gz
tar -xvzf test/generated/minisign.tar.gz -C test/generated
test/generated/minisign-linux/x86_64/minisign -Vm test/generated/*.txt -p test/minisign.pub
fi
Empty file added test/generated/.keep
Empty file.

0 comments on commit 3397ada

Please sign in to comment.