ssh_sig 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b3210fd902e6e37ef1e0d89964f2562daff95a412dae0ddf79d6a50c14704514
4
+ data.tar.gz: 5f209bdfad63784811c1f8de2eda27ee42df6e6cdffe4bd61a0aad8dfa7c5fd9
5
+ SHA512:
6
+ metadata.gz: c2ceddd49854f0640b1aa0b7f46b0ffeb160f2c7244460dc6eae84d5849f89b4d6455091d2c2e96145ca3c1321eae92a92c8503f9c1e19c2334aad1d6312e13d
7
+ data.tar.gz: d4b46151147347bc5e817b5f7ae671cf746ba47b16d307da9ad12a42421e29c9a3f56837efbec95f6f64236c2d62b537c3db657879d395d648455f53699f62d4
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ ---
2
+ inherit_gem:
3
+ gitlab-styles:
4
+ - rubocop-default.yml
5
+
6
+ AllCops:
7
+ NewCops: enable
8
+ TargetRubyVersion: 2.7
9
+ Exclude:
10
+ - 'vendor/**/*'
11
+ - 'tmp/**/*'
12
+ - 'bin/**/*'
13
+ CacheRootDirectory: tmp
14
+ MaxFilesInCache: 25000
15
+
16
+ Rails:
17
+ Enabled: false
18
+
19
+ CodeReuse/ActiveRecord:
20
+ Enabled: false
21
+
22
+ RSpec/MultipleMemoizedHelpers:
23
+ Max: 10
24
+
25
+ RSpec/NamedSubject:
26
+ Enabled: true
27
+
28
+ # Our heredoc delimiters do in fact contain files,
29
+ # so EOF is appropriate.
30
+ Naming/HeredocDelimiterNaming:
31
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.1] - 2021-12-06
4
+
5
+ - Fix gemspec
6
+
7
+ ## [0.1.0] - 2021-11-14
8
+
9
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in ssh_sig.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.0"
10
+ gem "pry", "~> 0.14.1"
data/Gemfile.lock ADDED
@@ -0,0 +1,115 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ssh_sig (0.1.1)
5
+ bcrypt_pbkdf (~> 1.1)
6
+ ed25519 (~> 1.2)
7
+ net-ssh (>= 6.3.0.beta1)
8
+ zeitwerk (~> 2.4)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activesupport (6.1.4.1)
14
+ concurrent-ruby (~> 1.0, >= 1.0.2)
15
+ i18n (>= 1.6, < 2)
16
+ minitest (>= 5.1)
17
+ tzinfo (~> 2.0)
18
+ zeitwerk (~> 2.3)
19
+ ast (2.4.2)
20
+ bcrypt_pbkdf (1.1.0)
21
+ binding_ninja (0.2.3)
22
+ coderay (1.1.3)
23
+ concurrent-ruby (1.1.9)
24
+ diff-lcs (1.4.4)
25
+ ed25519 (1.2.4)
26
+ gitlab-styles (6.2.1)
27
+ rubocop (~> 0.91, >= 0.91.1)
28
+ rubocop-gitlab-security (~> 0.1.1)
29
+ rubocop-performance (~> 1.9.2)
30
+ rubocop-rails (~> 2.9)
31
+ rubocop-rspec (~> 1.44)
32
+ i18n (1.8.11)
33
+ concurrent-ruby (~> 1.0)
34
+ method_source (1.0.0)
35
+ minitest (5.14.4)
36
+ net-ssh (6.3.0.beta1)
37
+ parallel (1.21.0)
38
+ parser (3.0.2.0)
39
+ ast (~> 2.4.1)
40
+ proc_to_ast (0.1.0)
41
+ coderay
42
+ parser
43
+ unparser
44
+ pry (0.14.1)
45
+ coderay (~> 1.1)
46
+ method_source (~> 1.0)
47
+ rack (2.2.3)
48
+ rainbow (3.0.0)
49
+ rake (13.0.6)
50
+ regexp_parser (2.1.1)
51
+ rexml (3.2.5)
52
+ rspec (3.10.0)
53
+ rspec-core (~> 3.10.0)
54
+ rspec-expectations (~> 3.10.0)
55
+ rspec-mocks (~> 3.10.0)
56
+ rspec-core (3.10.1)
57
+ rspec-support (~> 3.10.0)
58
+ rspec-expectations (3.10.1)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.10.0)
61
+ rspec-mocks (3.10.2)
62
+ diff-lcs (>= 1.2.0, < 2.0)
63
+ rspec-support (~> 3.10.0)
64
+ rspec-parameterized (0.5.0)
65
+ binding_ninja (>= 0.2.3)
66
+ parser
67
+ proc_to_ast
68
+ rspec (>= 2.13, < 4)
69
+ unparser
70
+ rspec-support (3.10.3)
71
+ rubocop (0.93.1)
72
+ parallel (~> 1.10)
73
+ parser (>= 2.7.1.5)
74
+ rainbow (>= 2.2.2, < 4.0)
75
+ regexp_parser (>= 1.8)
76
+ rexml
77
+ rubocop-ast (>= 0.6.0)
78
+ ruby-progressbar (~> 1.7)
79
+ unicode-display_width (>= 1.4.0, < 2.0)
80
+ rubocop-ast (1.13.0)
81
+ parser (>= 3.0.1.1)
82
+ rubocop-gitlab-security (0.1.1)
83
+ rubocop (>= 0.51)
84
+ rubocop-performance (1.9.2)
85
+ rubocop (>= 0.90.0, < 2.0)
86
+ rubocop-ast (>= 0.4.0)
87
+ rubocop-rails (2.9.1)
88
+ activesupport (>= 4.2.0)
89
+ rack (>= 1.1)
90
+ rubocop (>= 0.90.0, < 2.0)
91
+ rubocop-rspec (1.44.1)
92
+ rubocop (~> 0.87)
93
+ rubocop-ast (>= 0.7.1)
94
+ ruby-progressbar (1.11.0)
95
+ tzinfo (2.0.4)
96
+ concurrent-ruby (~> 1.0)
97
+ unicode-display_width (1.8.0)
98
+ unparser (0.6.2)
99
+ diff-lcs (~> 1.3)
100
+ parser (>= 3.0.0)
101
+ zeitwerk (2.5.1)
102
+
103
+ PLATFORMS
104
+ x86_64-darwin-20
105
+
106
+ DEPENDENCIES
107
+ gitlab-styles (~> 6.2.0)
108
+ pry (~> 0.14.1)
109
+ rake (~> 13.0)
110
+ rspec (~> 3.0)
111
+ rspec-parameterized (~> 0.5.0)
112
+ ssh_sig!
113
+
114
+ BUNDLED WITH
115
+ 2.2.30
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Brian Williams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # SshSig - SSH signature verification in pure ruby
2
+
3
+ SshSig is a Ruby gem which can be used to verify signatures signed created by `ssh-keygen`.
4
+ This capability was [first added](https://github.com/openssh/openssh-portable/commit/2a9c9f7272c1e8665155118fe6536bebdafb6166) in OpenSSH 8.0
5
+ allows SSH keys to be used for GPG-like signing capabilities, [including signing git commits](https://github.com/git/git/pull/1041).
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ssh_sig'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ssh_sig
22
+
23
+ ## Usage
24
+
25
+ Version 1 of [the SSH signature format](https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig)
26
+ supports `ed25519` and `rsa` keys. It is recommended that you use `ed25519` over `rsa` where possible (`ssh-keygen -t ed25519`).
27
+
28
+ In order to verify a signature you need:
29
+
30
+ 1. The public key of the sender
31
+ 1. The signature file
32
+ 1. The message to be verified.
33
+
34
+ ```ruby
35
+ require 'ssh_sig'
36
+
37
+ armored_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILXPkJPI4TMFWZP4xRBQjNeizUG99KuZCt9G23rX48kz"
38
+
39
+ blob = ::SshSig::Blob.from_armor(
40
+ <<~EOF
41
+ -----BEGIN SSH SIGNATURE-----
42
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgtc+Qk8jhMwVZk/jFEFCM16LNQb
43
+ 30q5kK30bbetfjyTMAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
44
+ OQAAAECJITeYJIlEeydsCTh1DkfdhlDJFBa73ojfWe0MbrIzoJKd9THd9WeQrhygSRGsNG
45
+ cU/stk3/919nykg67yG2gN
46
+ -----END SSH SIGNATURE-----
47
+ EOF
48
+ )
49
+
50
+ message = "This message was definitely sent by Brian Williams"
51
+
52
+ valid = ::SshSig::Verifier
53
+ .from_armored_pubkey(armored_pubkey)
54
+ .verify(blob, message)
55
+
56
+ if valid
57
+ puts 'Signature is valid'
58
+ else
59
+ puts 'Signature is not valid'
60
+ end
61
+ ```
62
+
63
+ Signatures can be created using `ssh-keygen -Y sign -n file -f ~/.ssh/ed_25519 message.txt`
64
+ and will be outputted in `message.txt.sig`.
65
+
66
+ Public keys can be found in a variety of places, including:
67
+
68
+ - Your `~/.ssh/id_<alg>.pub` file
69
+ - `authorized_keys` files on servers
70
+ - `https://gitlab.com/<username>.keys`
71
+ - `https://github.com/<username>.keys`
72
+
73
+ The `SshSig::Verifier#from_gitlab` and `SshSig::Verifier#from_github` methods are provided
74
+ to automatically load public keys from the respective `<username>.keys` urls.
75
+
76
+ ```ruby
77
+ require 'ssh_sig'
78
+
79
+ blob = ::SshSig::Blob.from_armor(
80
+ <<~EOF
81
+ -----BEGIN SSH SIGNATURE-----
82
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgtc+Qk8jhMwVZk/jFEFCM16LNQb
83
+ 30q5kK30bbetfjyTMAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
84
+ OQAAAECJITeYJIlEeydsCTh1DkfdhlDJFBa73ojfWe0MbrIzoJKd9THd9WeQrhygSRGsNG
85
+ cU/stk3/919nykg67yG2gN
86
+ -----END SSH SIGNATURE-----
87
+ EOF
88
+ )
89
+
90
+ message = 'This message was definitely sent by Brian Williams'
91
+
92
+ valid = ::SshSig::Verifier
93
+ .from_gitlab('bwill')
94
+ .verify(blob, message)
95
+
96
+ if valid
97
+ puts 'Signature is valid'
98
+ else
99
+ puts 'Signature is not valid'
100
+ end
101
+ ```
102
+
103
+ ## Is it safe to re-purpose SSH keys for signing?
104
+
105
+ Yes. The [SSH signature protocol](https://github.com/openssh/openssh-portable/blob/d575cf44895104e0fcb0629920fb645207218129/PROTOCOL.sshsig)
106
+ is designed to be resistant to cross-protocol attacks, where signatures created for one purpose (i.e. signing a git commit),
107
+ may be re-used for another purpose (i.e. authenticating to a server). It does this using the magic pre-amble (to differentiate
108
+ between messages signed by `ssh-keygen` and messages used for SSH authentication) and namespaces (to differentiate between
109
+ messages signed by `ssh-keygen` but used for different purposes). This causes identical messages to produce different signatures
110
+ for each different protocol.
111
+
112
+ ## Development
113
+
114
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
115
+
116
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
117
+
118
+ ## Contributing
119
+
120
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ssh_sig. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/ssh_sig/blob/main/CODE_OF_CONDUCT.md).
121
+
122
+ ## License
123
+
124
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ # Run both unit and integration tests
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ # Run tests excluding integration tests
10
+ RSpec::Core::RakeTask.new(:spec_unit) do |t|
11
+ t.rspec_opts = "--tag ~@integration"
12
+ end
13
+
14
+ # Run only integration tests
15
+ RSpec::Core::RakeTask.new(:spec_integration) do |t|
16
+ t.rspec_opts = "--tag integration"
17
+ end
18
+
19
+ require "rubocop/rake_task"
20
+
21
+ RuboCop::RakeTask.new
22
+
23
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ssh_sig'
6
+ require 'net/ssh'
7
+
8
+ # require 'pry'
9
+ # Pry.start
10
+
11
+ require_relative '../spec/support/helpers/byte_helpers'
12
+ # rubocop:disable Syle/MixinUsage
13
+ include ByteHelpers
14
+ # rubocop:enable Syle/MixinUsage
15
+
16
+ def hexdump(bytes)
17
+ puts bin_to_hex(bytes)
18
+ end
19
+
20
+ require 'irb'
21
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ echo "Installing OpenSSH version ${OPENSSH_VERSION?:OPENSSH_VERSION must be set}"
6
+
7
+ curl -sSL -o /tmp/openssh.tar.gz "https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-${OPENSSH_VERSION}.tar.gz"
8
+
9
+ tar -xzvf /tmp/openssh.tar.gz -C /tmp/
10
+
11
+ "/tmp/openssh-${OPENSSH_VERSION}/configure"
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ssh/buffer'
4
+ require 'digest'
5
+
6
+ module SshSig
7
+ class Blob
8
+ include Serializable
9
+ extend Serializable
10
+
11
+ attr_reader :namespace, :hash_algorithm, :signature
12
+
13
+ def initialize(
14
+ public_key:,
15
+ namespace:,
16
+ hash_algorithm:,
17
+ signature:
18
+ )
19
+ @public_key = public_key
20
+ @namespace = namespace
21
+ @hash_algorithm = hash_algorithm
22
+ @signature = signature
23
+ end
24
+
25
+ # public_key is parsed from the signature data and is untrusted
26
+ # We make this clear using accessor naming
27
+ def public_key_untrusted
28
+ @public_key
29
+ end
30
+
31
+ def self.from_armor(armor)
32
+ from_bytes(armor_to_blob(armor))
33
+ end
34
+
35
+ # decode_blob parses the binary signature data as described in
36
+ # https://github.com/openssh/openssh-portable/blob/e665ed2d0c24fe11d5470ce72fa1e187377d3fc4/PROTOCOL.sshsig
37
+ #
38
+ # byte[6] MAGIC_PREAMBLE
39
+ # uint32 SIG_VERSION
40
+ # string publickey
41
+ # string namespace
42
+ # string reserved
43
+ # string hash_algorithm
44
+ # string signature
45
+ def self.from_bytes(blob)
46
+ buf = ::Net::SSH::Buffer.new(blob)
47
+
48
+ preamble = buf.read!(6)
49
+
50
+ raise DecodeError, 'Invalid magic preamble' unless preamble == MAGIC_PREAMBLE
51
+
52
+ version = read_uint64(buf)
53
+
54
+ raise DecodeError, 'Unsupported signature version' unless version == SIG_VERSION
55
+
56
+ public_key = buf.read_key
57
+
58
+ raise DecodeError, 'Signature is missing public key' if public_key.nil?
59
+
60
+ namespace = buf.read_string
61
+
62
+ raise DecodeError, 'Signature is missing namespace' if namespace.nil?
63
+
64
+ # Read past the reserved value and ignore it.
65
+ buf.read_string
66
+
67
+ hash_algorithm = buf.read_string
68
+
69
+ raise DecodeError, 'Signature is missing hash algorithm' if hash_algorithm.nil?
70
+ raise DecodeError, 'Hash algorithm is not supported' unless hash_algorithm_allowed?(hash_algorithm)
71
+
72
+ signature_raw = buf.read_string
73
+
74
+ raise DecodeError, 'Signature is missing signed data' if signature_raw.nil?
75
+
76
+ signature = Signature.from_bytes(signature_raw)
77
+
78
+ raise DecodeError, 'Signature algorithm is not supported' \
79
+ unless signature_algorithm_allowed?(signature.algorithm)
80
+
81
+ Blob.new(
82
+ public_key: public_key,
83
+ namespace: namespace,
84
+ hash_algorithm: hash_algorithm,
85
+ signature: signature
86
+ )
87
+ end
88
+
89
+ # signature_data creates the "message" passed to the
90
+ # signing function as described in section 3 of
91
+ # https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig
92
+ #
93
+ # Despite the documentation's use of the word "concatenated",
94
+ # this data must use the same DER-like encoding as the signature blob.
95
+ #
96
+ # byte[6] MAGIC_PREAMBLE
97
+ # string namespace
98
+ # string reserved
99
+ # string hash_algorithm
100
+ # string H(message)
101
+ def signature_data(message)
102
+ buf = ::Net::SSH::Buffer.new
103
+
104
+ buf.write(MAGIC_PREAMBLE)
105
+ buf.write_string(namespace)
106
+ buf.write_string('') # reserved
107
+ buf.write_string(hash_algorithm)
108
+ buf.write_string(hash(message))
109
+
110
+ buf.to_s
111
+ end
112
+
113
+ private
114
+
115
+ def self.read_uint64(buf)
116
+ b = buf.read(8)
117
+
118
+ return nil unless b
119
+
120
+ b.unpack1("N")
121
+ end
122
+
123
+ def self.armor_to_blob(armor)
124
+ # Remove starting and ending whitespace for header checks.
125
+ armor = armor.strip
126
+
127
+ raise DecodeError, "Couldn't parse signature: missing header" unless armor.start_with?(BEGIN_SIGNATURE)
128
+
129
+ raise DecodeError, "Couldn't parse signature: missing footer" unless armor.end_with?(END_SIGNATURE)
130
+
131
+ b64 = armor
132
+ .delete_prefix(BEGIN_SIGNATURE)
133
+ .delete_suffix(END_SIGNATURE)
134
+ .gsub(/\s+/, '') # Remove all remaining whitespace to ensure valid Base64
135
+
136
+ begin
137
+ Base64.strict_decode64(b64)
138
+ rescue ArgumentError => e
139
+ raise DecodeError, "Couldn't decode armor body: #{e.message}"
140
+ end
141
+ end
142
+
143
+ def hash(data)
144
+ case hash_algorithm
145
+ when "sha512"
146
+ ::Digest::SHA2.new(512).digest(data)
147
+ when "sha256"
148
+ ::Digest::SHA2.new(256).digest(data)
149
+ else
150
+ raise VerifyError, "Hash algorithm #{hash_algorithm} is not supported"
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module SshSig
3
+ Error = Class.new(::StandardError)
4
+ DecodeError = Class.new(Error)
5
+ LoadError = Class.new(Error)
6
+ VerifyError = Class.new(Error)
7
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+
5
+ module SshSig
6
+ module KeyLoader
7
+ class Http < PubKey
8
+ class << self
9
+ def load(url)
10
+ keys = get(url)
11
+
12
+ super(keys)
13
+ end
14
+
15
+ def load_dot_keys(username, base_addr = 'https://gitlab.com')
16
+ load("#{base_addr}/#{username}.keys")
17
+ end
18
+
19
+ private
20
+
21
+ def get(url)
22
+ URI(url).read
23
+ rescue StandardError => e
24
+ raise ::SshSig::LoadError, "Error received from remote: #{e.message}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require 'net/ssh'
3
+
4
+ module SshSig
5
+ module KeyLoader
6
+ class PubKey
7
+ SUPPORTED_KEY_ALGORITHMS = %w[ssh-ed25519 ssh-rsa].freeze
8
+ class << self
9
+ def load(armored)
10
+ keys = armored.split("\n")
11
+
12
+ keys
13
+ .filter { |key| supported_key_algorithm?(key) }
14
+ .map { |key| load_data_public_key(key) }
15
+ rescue ::Net::SSH::Exception, ::ArgumentError
16
+ raise ::SshSig::LoadError, 'Public key is not valid'
17
+ end
18
+
19
+ private
20
+
21
+ def load_data_public_key(key)
22
+ ::Net::SSH::KeyFactory.load_data_public_key(key)
23
+ end
24
+
25
+ def supported_key_algorithm?(key)
26
+ alg = key.split(' ').first
27
+
28
+ SUPPORTED_KEY_ALGORITHMS.any?(alg)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module SshSig
3
+ module Serializable
4
+ SIG_VERSION = 1
5
+ MAGIC_PREAMBLE = "SSHSIG"
6
+ BEGIN_SIGNATURE = "-----BEGIN SSH SIGNATURE-----"
7
+ END_SIGNATURE = "-----END SSH SIGNATURE-----"
8
+ SIGALG_ALLOWED = %w[ssh-ed25519 rsa-sha2-512 rsa-sha2-256].freeze
9
+ HASHALG_ALLOWED = %w[sha512 sha256].freeze
10
+
11
+ def signature_algorithm_allowed?(alg)
12
+ SIGALG_ALLOWED.any?(alg)
13
+ end
14
+
15
+ def hash_algorithm_allowed?(alg)
16
+ HASHALG_ALLOWED.any?(alg)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ module SshSig
3
+ class Signature
4
+ attr_reader :algorithm, :bytes
5
+
6
+ def initialize(
7
+ algorithm:,
8
+ bytes:
9
+ )
10
+ @algorithm = algorithm
11
+ @bytes = bytes
12
+ end
13
+
14
+ # While not described well in the protocol documenation,
15
+ # the signature has two parts. There is a string describing the algorithm,
16
+ # which can be one of ssh-ed25519, rsa-sha2-512, or rsa-sha2-256,
17
+ # followed by the actual signature bytes.
18
+ #
19
+ # string algorithm
20
+ # string signature_bytes
21
+ def self.from_bytes(blob)
22
+ buf = ::Net::SSH::Buffer.new(blob)
23
+
24
+ algorithm = buf.read_string
25
+
26
+ raise DecodeError, 'Signature algorithm is missing' if algorithm.nil?
27
+
28
+ bytes = buf.read_string
29
+
30
+ raise DecodeError, 'Signature data is missing' if bytes.nil?
31
+
32
+ Signature.new(
33
+ algorithm: algorithm,
34
+ bytes: bytes
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ed25519'
4
+
5
+ module SshSig
6
+ class Verifier
7
+ def initialize(public_keys)
8
+ @public_keys = public_keys
9
+ end
10
+
11
+ def self.from_armored_pubkey(armored_pubkey)
12
+ public_keys = ::SshSig::KeyLoader::PubKey.load(armored_pubkey)
13
+
14
+ new(public_keys)
15
+ end
16
+
17
+ def self.from_github(username, base_addr = 'https://github.com')
18
+ public_keys = ::SshSig::KeyLoader::Http.load_dot_keys(username, base_addr)
19
+
20
+ new(public_keys)
21
+ end
22
+
23
+ def self.from_gitlab(username, base_addr = 'https://gitlab.com')
24
+ public_keys = ::SshSig::KeyLoader::Http.load_dot_keys(username, base_addr)
25
+
26
+ new(public_keys)
27
+ end
28
+
29
+ def verify(blob, message)
30
+ return false unless blob&.signature
31
+
32
+ @public_keys.any? do |key|
33
+ key.ssh_do_verify(
34
+ blob.signature.bytes,
35
+ blob.signature_data(message),
36
+ # When using RSA, net-ssh uses this to determine the digest algorithm to use.
37
+ # Added in net-ssh 6.3.0.beta1
38
+ { host_key: blob.signature.algorithm }
39
+ )
40
+ end
41
+ rescue ::Ed25519::VerifyError
42
+ # Ed25519 public keys raise exceptions when they fail to verify,
43
+ # but RSA public keys don't
44
+ false
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SshSig
4
+ VERSION = "0.1.1"
5
+ end
data/lib/ssh_sig.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.setup
7
+
8
+ module SshSig
9
+ end
10
+
11
+ loader.eager_load
metadata ADDED
@@ -0,0 +1,274 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ssh_sig
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Brian Williams
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bcrypt_pbkdf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ed25519
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-ssh
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 6.3.0.beta1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 6.3.0.beta1
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: gitlab-styles
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 6.2.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 6.2.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-parameterized
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.5.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.5.0
97
+ description: |
98
+ # SshSig - SSH signature verification in pure ruby
99
+
100
+ SshSig is a Ruby gem which can be used to verify signatures signed created by `ssh-keygen`.
101
+ This capability was [first added](https://github.com/openssh/openssh-portable/commit/2a9c9f7272c1e8665155118fe6536bebdafb6166) in OpenSSH 8.0
102
+ allows SSH keys to be used for GPG-like signing capabilities, [including signing git commits](https://github.com/git/git/pull/1041).
103
+
104
+ ## Installation
105
+
106
+ Add this line to your application's Gemfile:
107
+
108
+ ```ruby
109
+ gem 'ssh_sig'
110
+ ```
111
+
112
+ And then execute:
113
+
114
+ $ bundle install
115
+
116
+ Or install it yourself as:
117
+
118
+ $ gem install ssh_sig
119
+
120
+ ## Usage
121
+
122
+ Version 1 of [the SSH signature format](https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig)
123
+ supports `ed25519` and `rsa` keys. It is recommended that you use `ed25519` over `rsa` where possible (`ssh-keygen -t ed25519`).
124
+
125
+ In order to verify a signature you need:
126
+
127
+ 1. The public key of the sender
128
+ 1. The signature file
129
+ 1. The message to be verified.
130
+
131
+ ```ruby
132
+ require 'ssh_sig'
133
+
134
+ armored_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILXPkJPI4TMFWZP4xRBQjNeizUG99KuZCt9G23rX48kz"
135
+
136
+ blob = ::SshSig::Blob.from_armor(
137
+ <<~EOF
138
+ -----BEGIN SSH SIGNATURE-----
139
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgtc+Qk8jhMwVZk/jFEFCM16LNQb
140
+ 30q5kK30bbetfjyTMAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
141
+ OQAAAECJITeYJIlEeydsCTh1DkfdhlDJFBa73ojfWe0MbrIzoJKd9THd9WeQrhygSRGsNG
142
+ cU/stk3/919nykg67yG2gN
143
+ -----END SSH SIGNATURE-----
144
+ EOF
145
+ )
146
+
147
+ message = "This message was definitely sent by Brian Williams"
148
+
149
+ valid = ::SshSig::Verifier
150
+ .from_armored_pubkey(armored_pubkey)
151
+ .verify(blob, message)
152
+
153
+ if valid
154
+ puts 'Signature is valid'
155
+ else
156
+ puts 'Signature is not valid'
157
+ end
158
+ ```
159
+
160
+ Signatures can be created using `ssh-keygen -Y sign -n file -f ~/.ssh/ed_25519 message.txt`
161
+ and will be outputted in `message.txt.sig`.
162
+
163
+ Public keys can be found in a variety of places, including:
164
+
165
+ - Your `~/.ssh/id_<alg>.pub` file
166
+ - `authorized_keys` files on servers
167
+ - `https://gitlab.com/<username>.keys`
168
+ - `https://github.com/<username>.keys`
169
+
170
+ The `SshSig::Verifier#from_gitlab` and `SshSig::Verifier#from_github` methods are provided
171
+ to automatically load public keys from the respective `<username>.keys` urls.
172
+
173
+ ```ruby
174
+ require 'ssh_sig'
175
+
176
+ blob = ::SshSig::Blob.from_armor(
177
+ <<~EOF
178
+ -----BEGIN SSH SIGNATURE-----
179
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgtc+Qk8jhMwVZk/jFEFCM16LNQb
180
+ 30q5kK30bbetfjyTMAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
181
+ OQAAAECJITeYJIlEeydsCTh1DkfdhlDJFBa73ojfWe0MbrIzoJKd9THd9WeQrhygSRGsNG
182
+ cU/stk3/919nykg67yG2gN
183
+ -----END SSH SIGNATURE-----
184
+ EOF
185
+ )
186
+
187
+ message = 'This message was definitely sent by Brian Williams'
188
+
189
+ valid = ::SshSig::Verifier
190
+ .from_gitlab('bwill')
191
+ .verify(blob, message)
192
+
193
+ if valid
194
+ puts 'Signature is valid'
195
+ else
196
+ puts 'Signature is not valid'
197
+ end
198
+ ```
199
+
200
+ ## Is it safe to re-purpose SSH keys for signing?
201
+
202
+ Yes. The [SSH signature protocol](https://github.com/openssh/openssh-portable/blob/d575cf44895104e0fcb0629920fb645207218129/PROTOCOL.sshsig)
203
+ is designed to be resistant to cross-protocol attacks, where signatures created for one purpose (i.e. signing a git commit),
204
+ may be re-used for another purpose (i.e. authenticating to a server). It does this using the magic pre-amble (to differentiate
205
+ between messages signed by `ssh-keygen` and messages used for SSH authentication) and namespaces (to differentiate between
206
+ messages signed by `ssh-keygen` but used for different purposes). This causes identical messages to produce different signatures
207
+ for each different protocol.
208
+
209
+ ## Development
210
+
211
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
212
+
213
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
214
+
215
+ ## Contributing
216
+
217
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ssh_sig. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/ssh_sig/blob/main/CODE_OF_CONDUCT.md).
218
+
219
+ ## License
220
+
221
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
222
+ email:
223
+ - bwilliams@gitlab.com
224
+ executables: []
225
+ extensions: []
226
+ extra_rdoc_files: []
227
+ files:
228
+ - ".rspec"
229
+ - ".rubocop.yml"
230
+ - CHANGELOG.md
231
+ - Gemfile
232
+ - Gemfile.lock
233
+ - LICENSE.txt
234
+ - README.md
235
+ - Rakefile
236
+ - bin/console
237
+ - bin/setup
238
+ - bin/setup-integration
239
+ - lib/ssh_sig.rb
240
+ - lib/ssh_sig/blob.rb
241
+ - lib/ssh_sig/error.rb
242
+ - lib/ssh_sig/key_loader/http.rb
243
+ - lib/ssh_sig/key_loader/pub_key.rb
244
+ - lib/ssh_sig/serializable.rb
245
+ - lib/ssh_sig/signature.rb
246
+ - lib/ssh_sig/verifier.rb
247
+ - lib/ssh_sig/version.rb
248
+ homepage: https://gitlab.com/bwill/ssh_sig
249
+ licenses:
250
+ - MIT
251
+ metadata:
252
+ homepage_uri: https://gitlab.com/bwill/ssh_sig
253
+ source_code_uri: https://gitlab.com/bwill/ssh_sig
254
+ changelog_uri: https://gitlab.com/bwill/ssh_sig/-/blob/main/CHANGELOG.md
255
+ post_install_message:
256
+ rdoc_options: []
257
+ require_paths:
258
+ - lib
259
+ required_ruby_version: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - ">="
262
+ - !ruby/object:Gem::Version
263
+ version: 2.7.0
264
+ required_rubygems_version: !ruby/object:Gem::Requirement
265
+ requirements:
266
+ - - ">="
267
+ - !ruby/object:Gem::Version
268
+ version: '0'
269
+ requirements: []
270
+ rubygems_version: 3.1.6
271
+ signing_key:
272
+ specification_version: 4
273
+ summary: SSH Signatures in Ruby
274
+ test_files: []