openssl-ssh 0.1.0 → 0.2.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 +5 -5
- data/.github/workflows/ci.yml +44 -0
- data/.github/workflows/publish.yml +52 -0
- data/.gitignore +0 -1
- data/.standard.yml +8 -0
- data/.tool-versions +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +75 -25
- data/README.md +3 -1
- data/Rakefile +6 -3
- data/TODO.md +56 -0
- data/doc/openssh.gif +0 -0
- data/doc/openssl.png +0 -0
- data/doc/ruby.png +0 -0
- data/lib/openssl/ssh/version.rb +3 -1
- data/lib/openssl/ssh.rb +137 -34
- data/openssl-ssh.gemspec +32 -34
- data/openssl-ssh.md +30 -0
- metadata +57 -21
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 37a68dc478799667fdf5e6197816d68270485d49cdb3b5a55d4eafd5a6f26fe2
|
|
4
|
+
data.tar.gz: a735beabe82287963121cfd4e8aae2791072c7e49eb4ee5729746888d3d4900c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bea96bc4ed7eb74f70f4954ce92f56e7fae212a0b5a1a1eb0cad360a01df9102699335e461c2159699f414949f8680a458b5883335ab8833e38a77e88f2279f
|
|
7
|
+
data.tar.gz: 0fd6c0455b634116d8d82bf1f1af28db706c4325c24cf43d8a5b0482c567526bd941292f4693852a8f175b70514415ae9866aef94acfc0b5215db9b3dd36f877
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
ruby-version: ['3.0', '3.1', '3.2', '3.3']
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: bundle exec rspec
|
|
28
|
+
|
|
29
|
+
lint:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Set up Ruby
|
|
35
|
+
uses: ruby/setup-ruby@v1
|
|
36
|
+
with:
|
|
37
|
+
ruby-version: '3.3'
|
|
38
|
+
bundler-cache: true
|
|
39
|
+
|
|
40
|
+
- name: Run Standard RB
|
|
41
|
+
run: bundle exec standardrb --no-fix
|
|
42
|
+
|
|
43
|
+
- name: Check gem builds
|
|
44
|
+
run: gem build openssl-ssh.gemspec
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Ruby
|
|
20
|
+
uses: ruby/setup-ruby@v1
|
|
21
|
+
with:
|
|
22
|
+
ruby-version: '3.3'
|
|
23
|
+
|
|
24
|
+
- name: Extract version from tag
|
|
25
|
+
id: version
|
|
26
|
+
run: |
|
|
27
|
+
TAG=${GITHUB_REF#refs/tags/v}
|
|
28
|
+
echo "version=$TAG" >> $GITHUB_OUTPUT
|
|
29
|
+
if [[ "$TAG" =~ -pre[0-9]*$ ]] || [[ "$TAG" =~ -alpha[0-9]*$ ]] || [[ "$TAG" =~ -beta[0-9]*$ ]] || [[ "$TAG" =~ -rc[0-9]*$ ]] || [[ "$TAG" =~ -a[0-9]*$ ]]; then
|
|
30
|
+
echo "prerelease=true" >> $GITHUB_OUTPUT
|
|
31
|
+
else
|
|
32
|
+
echo "prerelease=false" >> $GITHUB_OUTPUT
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
- name: Build gem
|
|
36
|
+
id: build
|
|
37
|
+
run: |
|
|
38
|
+
gem build *.gemspec
|
|
39
|
+
echo "gem_file=$(ls *.gem)" >> $GITHUB_OUTPUT
|
|
40
|
+
|
|
41
|
+
- name: Configure RubyGems credentials
|
|
42
|
+
uses: rubygems/configure-rubygems-credentials@v1.0.0
|
|
43
|
+
|
|
44
|
+
- name: Publish to RubyGems
|
|
45
|
+
run: gem push ${{ steps.build.outputs.gem_file }}
|
|
46
|
+
|
|
47
|
+
- name: Create GitHub Release
|
|
48
|
+
uses: softprops/action-gh-release@v2
|
|
49
|
+
with:
|
|
50
|
+
files: ${{ steps.build.outputs.gem_file }}
|
|
51
|
+
prerelease: ${{ steps.version.outputs.prerelease }}
|
|
52
|
+
generate_release_notes: true
|
data/.gitignore
CHANGED
data/.standard.yml
ADDED
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.3.8
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,43 +1,93 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openssl-ssh (0.1
|
|
4
|
+
openssl-ssh (0.2.1)
|
|
5
|
+
base64
|
|
5
6
|
|
|
6
7
|
GEM
|
|
7
8
|
remote: https://rubygems.org/
|
|
8
9
|
specs:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
ast (2.4.3)
|
|
11
|
+
base64 (0.3.0)
|
|
12
|
+
diff-lcs (1.6.2)
|
|
13
|
+
docile (1.4.1)
|
|
14
|
+
json (2.18.1)
|
|
15
|
+
language_server-protocol (3.17.0.5)
|
|
16
|
+
lint_roller (1.1.0)
|
|
17
|
+
parallel (1.27.0)
|
|
18
|
+
parser (3.3.10.1)
|
|
19
|
+
ast (~> 2.4.1)
|
|
20
|
+
racc
|
|
21
|
+
prism (1.9.0)
|
|
22
|
+
racc (1.8.1)
|
|
23
|
+
rainbow (3.1.1)
|
|
24
|
+
rake (13.3.1)
|
|
25
|
+
regexp_parser (2.11.3)
|
|
26
|
+
rspec (3.13.2)
|
|
27
|
+
rspec-core (~> 3.13.0)
|
|
28
|
+
rspec-expectations (~> 3.13.0)
|
|
29
|
+
rspec-mocks (~> 3.13.0)
|
|
30
|
+
rspec-core (3.13.6)
|
|
31
|
+
rspec-support (~> 3.13.0)
|
|
32
|
+
rspec-expectations (3.13.5)
|
|
20
33
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
21
|
-
rspec-support (~> 3.
|
|
22
|
-
rspec-mocks (3.
|
|
34
|
+
rspec-support (~> 3.13.0)
|
|
35
|
+
rspec-mocks (3.13.7)
|
|
23
36
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
24
|
-
rspec-support (~> 3.
|
|
25
|
-
rspec-support (3.
|
|
26
|
-
|
|
37
|
+
rspec-support (~> 3.13.0)
|
|
38
|
+
rspec-support (3.13.7)
|
|
39
|
+
rubocop (1.82.1)
|
|
40
|
+
json (~> 2.3)
|
|
41
|
+
language_server-protocol (~> 3.17.0.2)
|
|
42
|
+
lint_roller (~> 1.1.0)
|
|
43
|
+
parallel (~> 1.10)
|
|
44
|
+
parser (>= 3.3.0.2)
|
|
45
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
46
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
47
|
+
rubocop-ast (>= 1.48.0, < 2.0)
|
|
48
|
+
ruby-progressbar (~> 1.7)
|
|
49
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
50
|
+
rubocop-ast (1.49.0)
|
|
51
|
+
parser (>= 3.3.7.2)
|
|
52
|
+
prism (~> 1.7)
|
|
53
|
+
rubocop-performance (1.26.1)
|
|
54
|
+
lint_roller (~> 1.1)
|
|
55
|
+
rubocop (>= 1.75.0, < 2.0)
|
|
56
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
57
|
+
ruby-progressbar (1.13.0)
|
|
58
|
+
simplecov (0.22.0)
|
|
27
59
|
docile (~> 1.1)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
simplecov-html (0.
|
|
60
|
+
simplecov-html (~> 0.11)
|
|
61
|
+
simplecov_json_formatter (~> 0.1)
|
|
62
|
+
simplecov-html (0.13.2)
|
|
63
|
+
simplecov_json_formatter (0.1.4)
|
|
64
|
+
standard (1.53.0)
|
|
65
|
+
language_server-protocol (~> 3.17.0.2)
|
|
66
|
+
lint_roller (~> 1.0)
|
|
67
|
+
rubocop (~> 1.82.0)
|
|
68
|
+
standard-custom (~> 1.0.0)
|
|
69
|
+
standard-performance (~> 1.8)
|
|
70
|
+
standard-custom (1.0.2)
|
|
71
|
+
lint_roller (~> 1.0)
|
|
72
|
+
rubocop (~> 1.50)
|
|
73
|
+
standard-performance (1.9.0)
|
|
74
|
+
lint_roller (~> 1.1)
|
|
75
|
+
rubocop-performance (~> 1.26.0)
|
|
76
|
+
unicode-display_width (3.2.0)
|
|
77
|
+
unicode-emoji (~> 4.1)
|
|
78
|
+
unicode-emoji (4.2.0)
|
|
31
79
|
|
|
32
80
|
PLATFORMS
|
|
81
|
+
arm64-darwin-25
|
|
33
82
|
ruby
|
|
34
83
|
|
|
35
84
|
DEPENDENCIES
|
|
36
|
-
bundler (~>
|
|
85
|
+
bundler (~> 2.5)
|
|
37
86
|
openssl-ssh!
|
|
38
|
-
rake (~>
|
|
39
|
-
rspec (~> 3.
|
|
40
|
-
simplecov (~> 0.
|
|
87
|
+
rake (~> 13.2)
|
|
88
|
+
rspec (~> 3.13)
|
|
89
|
+
simplecov (~> 0.22)
|
|
90
|
+
standard (~> 1.53)
|
|
41
91
|
|
|
42
92
|
BUNDLED WITH
|
|
43
|
-
|
|
93
|
+
2.7.2
|
data/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
#  +  4 
|
|
2
|
+
|
|
1
3
|
[](https://codeclimate.com/github/aladac/openssl-ssh/maintainability)
|
|
2
4
|
[](https://codeclimate.com/github/aladac/openssl-ssh/test_coverage)
|
|
3
5
|
|
|
4
|
-
# OpenSSL::SSH
|
|
6
|
+
# OpenSSL::PKey::SSH
|
|
5
7
|
|
|
6
8
|
This gem introduces `OpenSSL::PKey::SSH` class with the ability to parse **OpenSSH** format public keys and return a correct `OpenSSL::PKey` type object. This is a convenience class used to parse the specific format of the **OpenSSH** public key.
|
|
7
9
|
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "standard/rake"
|
|
3
6
|
|
|
4
7
|
RSpec::Core::RakeTask.new(:spec)
|
|
5
8
|
|
|
6
|
-
task default:
|
|
9
|
+
task default: %i[spec standard]
|
data/TODO.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# OpenSSL-SSH - Modernization for Ruby 3.x / OpenSSL 3.x
|
|
2
|
+
|
|
3
|
+
Parse OpenSSH format public keys (`ssh-rsa AAAA...`) and return `OpenSSL::PKey` objects.
|
|
4
|
+
|
|
5
|
+
## Completed
|
|
6
|
+
|
|
7
|
+
### Phase 1: Fix Core
|
|
8
|
+
- [x] Replace property assignment with ASN.1 DER construction
|
|
9
|
+
- [x] Update `build_rsa` method to build ASN.1 sequence
|
|
10
|
+
- [x] Update `build_dsa` method to use SubjectPublicKeyInfo format
|
|
11
|
+
- [x] Test with Ruby 3.x / OpenSSL 3.x
|
|
12
|
+
- [x] Update specs
|
|
13
|
+
|
|
14
|
+
### Phase 2: Add Key Types
|
|
15
|
+
- [x] Ed25519 support (default for modern OpenSSH)
|
|
16
|
+
```ruby
|
|
17
|
+
OpenSSL::PKey.new_raw_public_key('ED25519', raw_key_bytes)
|
|
18
|
+
```
|
|
19
|
+
- [x] ECDSA support (ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521)
|
|
20
|
+
- [x] Keep RSA and DSA support
|
|
21
|
+
|
|
22
|
+
### Phase 3: Modernize
|
|
23
|
+
- [x] Update gemspec dependencies
|
|
24
|
+
- bundler ~> 2.0
|
|
25
|
+
- rake ~> 13.0
|
|
26
|
+
- rspec ~> 3.12
|
|
27
|
+
- simplecov ~> 0.22
|
|
28
|
+
- base64 (runtime dependency for Ruby 3.4+)
|
|
29
|
+
- [x] Add required Ruby version (>= 3.0)
|
|
30
|
+
- [x] Add GitHub Actions workflow
|
|
31
|
+
- [x] Update metadata URIs
|
|
32
|
+
|
|
33
|
+
## Future Improvements
|
|
34
|
+
|
|
35
|
+
### Phase 4: OpenSSH Private Key Format
|
|
36
|
+
- [ ] Support OpenSSH private key format (new format, not just PEM)
|
|
37
|
+
- Ed25519 and ECDSA private keys use new format by default
|
|
38
|
+
- Requires custom parser for `-----BEGIN OPENSSH PRIVATE KEY-----`
|
|
39
|
+
- [ ] Add fingerprint generation (MD5, SHA256)
|
|
40
|
+
- [ ] Add key comment extraction
|
|
41
|
+
- [ ] Better error messages
|
|
42
|
+
|
|
43
|
+
## Supported Key Types
|
|
44
|
+
|
|
45
|
+
| Type | Public Key | Private Key (PEM) | Private Key (OpenSSH) |
|
|
46
|
+
|------|------------|-------------------|----------------------|
|
|
47
|
+
| RSA | ✅ | ✅ | ❌ |
|
|
48
|
+
| DSA | ✅ | ✅ | ❌ |
|
|
49
|
+
| Ed25519 | ✅ | N/A | ❌ (pending) |
|
|
50
|
+
| ECDSA | ✅ | ✅ | ❌ (pending) |
|
|
51
|
+
|
|
52
|
+
## References
|
|
53
|
+
|
|
54
|
+
- [OpenSSL::PKey docs](https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/PKey.html)
|
|
55
|
+
- [RFC 4253 - SSH Transport](https://tools.ietf.org/html/rfc4253#section-6.6) - Key format
|
|
56
|
+
- [RFC 8709 - Ed25519 for SSH](https://tools.ietf.org/html/rfc8709)
|
data/doc/openssh.gif
ADDED
|
Binary file
|
data/doc/openssl.png
ADDED
|
Binary file
|
data/doc/ruby.png
ADDED
|
Binary file
|
data/lib/openssl/ssh/version.rb
CHANGED
data/lib/openssl/ssh.rb
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl/ssh/version"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "openssl"
|
|
4
6
|
|
|
5
7
|
module OpenSSL
|
|
6
8
|
module PKey
|
|
7
9
|
class SSH
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
SUPPORTED_TYPES = {
|
|
11
|
+
"ssh-rsa" => :rsa,
|
|
12
|
+
"ssh-dss" => :dsa,
|
|
13
|
+
"ssh-ed25519" => :ed25519,
|
|
14
|
+
"ecdsa-sha2-nistp256" => :ecdsa,
|
|
15
|
+
"ecdsa-sha2-nistp384" => :ecdsa,
|
|
16
|
+
"ecdsa-sha2-nistp521" => :ecdsa
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
ECDSA_CURVES = {
|
|
20
|
+
"ecdsa-sha2-nistp256" => "prime256v1",
|
|
21
|
+
"ecdsa-sha2-nistp384" => "secp384r1",
|
|
22
|
+
"ecdsa-sha2-nistp521" => "secp521r1"
|
|
23
|
+
}.freeze
|
|
10
24
|
|
|
11
25
|
def self.new(key, password = nil)
|
|
12
26
|
forward_private_key(key, password) || parse_public_ssh_key(key)
|
|
@@ -14,63 +28,152 @@ module OpenSSL
|
|
|
14
28
|
|
|
15
29
|
def self.forward_private_key(key, password)
|
|
16
30
|
case key
|
|
17
|
-
when /BEGIN RSA PRIVATE KEY/
|
|
31
|
+
when /BEGIN RSA PRIVATE KEY/, /BEGIN PRIVATE KEY/
|
|
18
32
|
OpenSSL::PKey::RSA.new(key, password)
|
|
19
33
|
when /BEGIN DSA PRIVATE KEY/
|
|
20
34
|
OpenSSL::PKey::DSA.new(key, password)
|
|
35
|
+
when /BEGIN EC PRIVATE KEY/
|
|
36
|
+
OpenSSL::PKey::EC.new(key, password)
|
|
37
|
+
when /BEGIN OPENSSH PRIVATE KEY/
|
|
38
|
+
parse_openssh_private_key(key, password)
|
|
21
39
|
end
|
|
22
40
|
end
|
|
23
41
|
|
|
42
|
+
def self.parse_openssh_private_key(key, password)
|
|
43
|
+
# OpenSSH new format private keys - delegate to OpenSSL if supported
|
|
44
|
+
OpenSSL::PKey.read(key, password)
|
|
45
|
+
rescue OpenSSL::PKey::PKeyError
|
|
46
|
+
raise "OpenSSH private key format not supported by your OpenSSL version"
|
|
47
|
+
end
|
|
48
|
+
|
|
24
49
|
def self.parse_public_ssh_key(key)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
return key unless key.is_a?(String) && key.match?(/^(ssh-|ecdsa-)/)
|
|
51
|
+
|
|
52
|
+
parts = key.strip.split(" ", 3)
|
|
53
|
+
type = parts[0]
|
|
54
|
+
data = parts[1]
|
|
55
|
+
# comment = parts[2] # Available if needed
|
|
56
|
+
|
|
57
|
+
raise "Unsupported key type: #{type}" unless SUPPORTED_TYPES.key?(type)
|
|
58
|
+
|
|
59
|
+
decoded = Base64.decode64(data)
|
|
60
|
+
components = unpack_pubkey_components(decoded)
|
|
61
|
+
|
|
62
|
+
case SUPPORTED_TYPES[type]
|
|
63
|
+
when :rsa then build_rsa(components)
|
|
64
|
+
when :dsa then build_dsa(components)
|
|
65
|
+
when :ed25519 then build_ed25519(components)
|
|
66
|
+
when :ecdsa then build_ecdsa(components, type)
|
|
28
67
|
end
|
|
29
|
-
key
|
|
30
68
|
end
|
|
31
69
|
|
|
32
|
-
def self.
|
|
33
|
-
|
|
34
|
-
|
|
70
|
+
def self.build_rsa(components)
|
|
71
|
+
_type, e_bytes, n_bytes = components
|
|
72
|
+
e = bytes_to_bn(e_bytes)
|
|
73
|
+
n = bytes_to_bn(n_bytes)
|
|
35
74
|
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
# RSA public key ASN.1 structure:
|
|
76
|
+
# RSAPublicKey ::= SEQUENCE {
|
|
77
|
+
# modulus INTEGER, -- n
|
|
78
|
+
# publicExponent INTEGER -- e
|
|
79
|
+
# }
|
|
80
|
+
asn1 = OpenSSL::ASN1::Sequence.new([
|
|
81
|
+
OpenSSL::ASN1::Integer.new(n),
|
|
82
|
+
OpenSSL::ASN1::Integer.new(e)
|
|
83
|
+
])
|
|
84
|
+
OpenSSL::PKey::RSA.new(asn1.to_der)
|
|
38
85
|
end
|
|
39
86
|
|
|
40
|
-
def self.
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
def self.build_dsa(components)
|
|
88
|
+
_type, p_bytes, q_bytes, g_bytes, pub_key_bytes = components
|
|
89
|
+
p = bytes_to_bn(p_bytes)
|
|
90
|
+
q = bytes_to_bn(q_bytes)
|
|
91
|
+
g = bytes_to_bn(g_bytes)
|
|
92
|
+
pub_key = bytes_to_bn(pub_key_bytes)
|
|
43
93
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
94
|
+
# DSA public key requires SubjectPublicKeyInfo format:
|
|
95
|
+
# DSAParameters ::= SEQUENCE {
|
|
96
|
+
# p INTEGER,
|
|
97
|
+
# q INTEGER,
|
|
98
|
+
# g INTEGER
|
|
99
|
+
# }
|
|
100
|
+
# DSAPublicKey ::= INTEGER -- public key y
|
|
101
|
+
#
|
|
102
|
+
# SubjectPublicKeyInfo ::= SEQUENCE {
|
|
103
|
+
# algorithm AlgorithmIdentifier,
|
|
104
|
+
# subjectPublicKey BIT STRING
|
|
105
|
+
# }
|
|
106
|
+
dsa_params = OpenSSL::ASN1::Sequence.new([
|
|
107
|
+
OpenSSL::ASN1::Integer.new(p),
|
|
108
|
+
OpenSSL::ASN1::Integer.new(q),
|
|
109
|
+
OpenSSL::ASN1::Integer.new(g)
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
# OID for DSA: 1.2.840.10040.4.1
|
|
113
|
+
algo_id = OpenSSL::ASN1::Sequence.new([
|
|
114
|
+
OpenSSL::ASN1::ObjectId.new("DSA"),
|
|
115
|
+
dsa_params
|
|
116
|
+
])
|
|
47
117
|
|
|
48
|
-
|
|
49
|
-
|
|
118
|
+
pub_key_asn1 = OpenSSL::ASN1::Integer.new(pub_key)
|
|
119
|
+
pub_key_bitstring = OpenSSL::ASN1::BitString.new(pub_key_asn1.to_der)
|
|
120
|
+
|
|
121
|
+
spki = OpenSSL::ASN1::Sequence.new([
|
|
122
|
+
algo_id,
|
|
123
|
+
pub_key_bitstring
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
OpenSSL::PKey::DSA.new(spki.to_der)
|
|
50
127
|
end
|
|
51
128
|
|
|
52
|
-
def self.
|
|
53
|
-
|
|
54
|
-
next unless o.first.is_a? Symbol
|
|
129
|
+
def self.build_ed25519(components)
|
|
130
|
+
_type, raw_key = components
|
|
55
131
|
|
|
56
|
-
|
|
132
|
+
# Ed25519 requires Ruby 3.0+ with OpenSSL 1.1.1+
|
|
133
|
+
unless OpenSSL::PKey.respond_to?(:new_raw_public_key)
|
|
134
|
+
raise "Ed25519 requires Ruby 3.0+ with OpenSSL 1.1.1+"
|
|
57
135
|
end
|
|
58
|
-
|
|
136
|
+
|
|
137
|
+
OpenSSL::PKey.new_raw_public_key("ED25519", raw_key)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.build_ecdsa(components, key_type)
|
|
141
|
+
_type, _, point_data = components
|
|
142
|
+
curve = ECDSA_CURVES[key_type]
|
|
143
|
+
|
|
144
|
+
# ECDSA public key in SubjectPublicKeyInfo format
|
|
145
|
+
# The point_data is already in uncompressed point format (04 || x || y)
|
|
146
|
+
|
|
147
|
+
# OID for EC: 1.2.840.10045.2.1
|
|
148
|
+
# Named curve OIDs
|
|
149
|
+
algo_id = OpenSSL::ASN1::Sequence.new([
|
|
150
|
+
OpenSSL::ASN1::ObjectId.new("id-ecPublicKey"),
|
|
151
|
+
OpenSSL::ASN1::ObjectId.new(curve)
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
pub_key_bitstring = OpenSSL::ASN1::BitString.new(point_data)
|
|
155
|
+
|
|
156
|
+
spki = OpenSSL::ASN1::Sequence.new([
|
|
157
|
+
algo_id,
|
|
158
|
+
pub_key_bitstring
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
OpenSSL::PKey::EC.new(spki.to_der)
|
|
59
162
|
end
|
|
60
163
|
|
|
61
164
|
def self.unpack_pubkey_components(str)
|
|
62
|
-
|
|
165
|
+
components = []
|
|
63
166
|
i = 0
|
|
64
167
|
while i < str.length
|
|
65
|
-
len = str[i, 4].unpack1(
|
|
66
|
-
|
|
168
|
+
len = str[i, 4].unpack1("N")
|
|
169
|
+
components << str[i + 4, len]
|
|
67
170
|
i += 4 + len
|
|
68
171
|
end
|
|
69
|
-
|
|
172
|
+
components
|
|
70
173
|
end
|
|
71
174
|
|
|
72
|
-
def self.
|
|
73
|
-
|
|
175
|
+
def self.bytes_to_bn(bytes)
|
|
176
|
+
OpenSSL::BN.new(bytes, 2)
|
|
74
177
|
end
|
|
75
178
|
end
|
|
76
179
|
end
|
data/openssl-ssh.gemspec
CHANGED
|
@@ -1,42 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
-
require
|
|
5
|
+
require "openssl/ssh/version"
|
|
4
6
|
|
|
5
7
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name
|
|
7
|
-
spec.version
|
|
8
|
-
spec.authors
|
|
9
|
-
spec.email
|
|
10
|
-
|
|
11
|
-
spec.summary
|
|
12
|
-
spec.description
|
|
13
|
-
spec.homepage
|
|
14
|
-
spec.license
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here."
|
|
23
|
-
spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
|
|
24
|
-
else
|
|
25
|
-
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
|
26
|
-
'public gem pushes.'
|
|
27
|
-
end
|
|
8
|
+
spec.name = "openssl-ssh"
|
|
9
|
+
spec.version = OpenSSL::SSH::VERSION
|
|
10
|
+
spec.authors = ["Adam Ladachowski"]
|
|
11
|
+
spec.email = ["adam.ladachowski@gmail.com"]
|
|
12
|
+
|
|
13
|
+
spec.summary = "Handling for OpenSSH public keys"
|
|
14
|
+
spec.description = "Parse OpenSSH format public keys (ssh-rsa, ssh-dss, ssh-ed25519, ecdsa-sha2-*) and return OpenSSL::PKey objects"
|
|
15
|
+
spec.homepage = "https://github.com/aladac/openssl-ssh"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
|
|
18
|
+
spec.required_ruby_version = ">= 3.0"
|
|
19
|
+
|
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/aladac/openssl-ssh"
|
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/aladac/openssl-ssh/blob/master/CHANGELOG.md"
|
|
23
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
28
24
|
|
|
29
|
-
# Specify which files should be added to the gem when it is released.
|
|
30
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
31
25
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
32
26
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
33
27
|
end
|
|
34
|
-
spec.bindir
|
|
35
|
-
spec.executables
|
|
36
|
-
spec.require_paths = [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
spec.
|
|
40
|
-
|
|
41
|
-
spec.add_development_dependency
|
|
28
|
+
spec.bindir = "exe"
|
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
# Runtime dependencies (base64 extracted from stdlib in Ruby 3.4)
|
|
33
|
+
spec.add_dependency "base64"
|
|
34
|
+
|
|
35
|
+
spec.add_development_dependency "bundler", "~> 2.5"
|
|
36
|
+
spec.add_development_dependency "rake", "~> 13.2"
|
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
38
|
+
spec.add_development_dependency "simplecov", "~> 0.22"
|
|
39
|
+
spec.add_development_dependency "standard", "~> 1.53"
|
|
42
40
|
end
|
data/openssl-ssh.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# OpenSSL-SSH - Ruby Gem Modernization
|
|
2
|
+
|
|
3
|
+
**Repo**: https://github.com/aladac/openssl-ssh
|
|
4
|
+
**Detailed TODO**: See [TODO.md in repo](https://github.com/aladac/openssl-ssh/blob/master/TODO.md)
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
Ruby gem to parse OpenSSH format keys (`ssh-rsa AAAA...`) into `OpenSSL::PKey` objects.
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
Current code is **broken** on Ruby 2.4+ / OpenSSL 2.0+. Uses property assignment on immutable objects:
|
|
11
|
+
```ruby
|
|
12
|
+
key.n = value # ❌ Fails - OpenSSL::PKey is immutable
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
Use ASN.1 DER construction:
|
|
17
|
+
```ruby
|
|
18
|
+
asn1 = OpenSSL::ASN1::Sequence.new([
|
|
19
|
+
OpenSSL::ASN1::Integer.new(n),
|
|
20
|
+
OpenSSL::ASN1::Integer.new(e)
|
|
21
|
+
])
|
|
22
|
+
key = OpenSSL::PKey::RSA.new(asn1.to_der) # ✅ Works
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Tasks
|
|
26
|
+
- [ ] Replace property assignment with ASN.1 DER approach
|
|
27
|
+
- [ ] Add Ed25519 support (modern default)
|
|
28
|
+
- [ ] Add ECDSA support
|
|
29
|
+
- [ ] Update gemspec for Ruby 3.x
|
|
30
|
+
- [ ] Add GitHub Actions CI
|
metadata
CHANGED
|
@@ -1,100 +1,137 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openssl-ssh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Ladachowski
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: bundler
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
16
30
|
requirements:
|
|
17
31
|
- - "~>"
|
|
18
32
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
33
|
+
version: '2.5'
|
|
20
34
|
type: :development
|
|
21
35
|
prerelease: false
|
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
37
|
requirements:
|
|
24
38
|
- - "~>"
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
40
|
+
version: '2.5'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rake
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
30
44
|
requirements:
|
|
31
45
|
- - "~>"
|
|
32
46
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
47
|
+
version: '13.2'
|
|
34
48
|
type: :development
|
|
35
49
|
prerelease: false
|
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
51
|
requirements:
|
|
38
52
|
- - "~>"
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
54
|
+
version: '13.2'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: rspec
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
44
58
|
requirements:
|
|
45
59
|
- - "~>"
|
|
46
60
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '3.
|
|
61
|
+
version: '3.13'
|
|
48
62
|
type: :development
|
|
49
63
|
prerelease: false
|
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
65
|
requirements:
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '3.
|
|
68
|
+
version: '3.13'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: simplecov
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
58
72
|
requirements:
|
|
59
73
|
- - "~>"
|
|
60
74
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0.
|
|
75
|
+
version: '0.22'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.22'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: standard
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.53'
|
|
62
90
|
type: :development
|
|
63
91
|
prerelease: false
|
|
64
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
93
|
requirements:
|
|
66
94
|
- - "~>"
|
|
67
95
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
69
|
-
description:
|
|
70
|
-
and
|
|
96
|
+
version: '1.53'
|
|
97
|
+
description: Parse OpenSSH format public keys (ssh-rsa, ssh-dss, ssh-ed25519, ecdsa-sha2-*)
|
|
98
|
+
and return OpenSSL::PKey objects
|
|
71
99
|
email:
|
|
72
100
|
- adam.ladachowski@gmail.com
|
|
73
101
|
executables: []
|
|
74
102
|
extensions: []
|
|
75
103
|
extra_rdoc_files: []
|
|
76
104
|
files:
|
|
105
|
+
- ".github/workflows/ci.yml"
|
|
106
|
+
- ".github/workflows/publish.yml"
|
|
77
107
|
- ".gitignore"
|
|
78
108
|
- ".rspec"
|
|
79
|
-
- ".
|
|
109
|
+
- ".standard.yml"
|
|
110
|
+
- ".tool-versions"
|
|
80
111
|
- Gemfile
|
|
81
112
|
- Gemfile.lock
|
|
82
113
|
- LICENSE.txt
|
|
83
114
|
- README.md
|
|
84
115
|
- Rakefile
|
|
116
|
+
- TODO.md
|
|
85
117
|
- bin/console
|
|
86
118
|
- bin/setup
|
|
119
|
+
- doc/openssh.gif
|
|
120
|
+
- doc/openssl.png
|
|
121
|
+
- doc/ruby.png
|
|
87
122
|
- lib/openssl/ssh.rb
|
|
88
123
|
- lib/openssl/ssh/version.rb
|
|
89
124
|
- openssl-ssh.gemspec
|
|
125
|
+
- openssl-ssh.md
|
|
90
126
|
homepage: https://github.com/aladac/openssl-ssh
|
|
91
127
|
licenses:
|
|
92
128
|
- MIT
|
|
93
129
|
metadata:
|
|
94
130
|
homepage_uri: https://github.com/aladac/openssl-ssh
|
|
95
|
-
source_code_uri:
|
|
96
|
-
changelog_uri:
|
|
97
|
-
|
|
131
|
+
source_code_uri: https://github.com/aladac/openssl-ssh
|
|
132
|
+
changelog_uri: https://github.com/aladac/openssl-ssh/blob/master/CHANGELOG.md
|
|
133
|
+
rubygems_mfa_required: 'true'
|
|
134
|
+
post_install_message:
|
|
98
135
|
rdoc_options: []
|
|
99
136
|
require_paths:
|
|
100
137
|
- lib
|
|
@@ -102,16 +139,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
102
139
|
requirements:
|
|
103
140
|
- - ">="
|
|
104
141
|
- !ruby/object:Gem::Version
|
|
105
|
-
version: '0'
|
|
142
|
+
version: '3.0'
|
|
106
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
144
|
requirements:
|
|
108
145
|
- - ">="
|
|
109
146
|
- !ruby/object:Gem::Version
|
|
110
147
|
version: '0'
|
|
111
148
|
requirements: []
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
signing_key:
|
|
149
|
+
rubygems_version: 3.5.22
|
|
150
|
+
signing_key:
|
|
115
151
|
specification_version: 4
|
|
116
152
|
summary: Handling for OpenSSH public keys
|
|
117
153
|
test_files: []
|