ssh_sig 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []