opentofu 0.1.1 → 0.2.0
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 +4 -4
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +11 -0
- data/README.md +1 -1
- data/exe/tofu +38 -0
- data/lib/net/tofu/error.rb +9 -0
- data/lib/net/tofu/pub.rb +97 -0
- data/lib/net/tofu/request.rb +2 -2
- data/lib/net/tofu/socket.rb +34 -1
- data/lib/net/tofu/version.rb +1 -1
- data/lib/net/tofu.rb +6 -4
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1af62b16fa0e24d835f5255f5fc81526831f2ebbb06730ca257940057ff5e4b9
|
4
|
+
data.tar.gz: 2adbc048e86b177b764cfc9ec874f99142e814eb16de722779207e425cb87452
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa68f3a49de9f18d8a3a700757fd974528d1a503f562084d9e28385ac8aaadb4f6398fcd3e271643dc595b9308cdf91520cd9e9cb829113647025d458b68bb28
|
7
|
+
data.tar.gz: 12fe5263618be4a044d585b5f573248550ec8a91318e80c444e77158e79c18832390f3439e772cd4cb4ed0bb891128426667688aae1915bf00a7001828848e8d
|
data/.rubocop.yml
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
AllCops:
|
2
2
|
TargetRubyVersion: 2.6
|
3
3
|
|
4
|
+
Layout/HeredocIndentation:
|
5
|
+
Enabled: false
|
6
|
+
|
4
7
|
Metrics/AbcSize:
|
5
8
|
Enabled: true
|
6
9
|
Max: 25
|
@@ -9,6 +12,9 @@ Metrics/MethodLength:
|
|
9
12
|
Enabled: true
|
10
13
|
Max: 18
|
11
14
|
|
15
|
+
Style/Next:
|
16
|
+
Enabled: false
|
17
|
+
|
12
18
|
Style/Semicolon:
|
13
19
|
Enabled: true
|
14
20
|
Exclude:
|
@@ -27,3 +33,5 @@ Style/StringLiteralsInInterpolation:
|
|
27
33
|
|
28
34
|
Layout/LineLength:
|
29
35
|
Max: 120
|
36
|
+
Exclude:
|
37
|
+
- "test/test_net_tofu_pub.rb"
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.2.0] - 2023-07-29
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Certificate checking and pinning logic.
|
13
|
+
- Tests for certificate and pinning logic.
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
|
17
|
+
- Tests involving fetching data, as they need to trust the host now.
|
18
|
+
|
8
19
|
## [0.1.1] - 2023-07-28
|
9
20
|
|
10
21
|
### Added
|
data/README.md
CHANGED
data/exe/tofu
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "net/tofu"
|
5
|
+
|
6
|
+
OptionParser.new do |p|
|
7
|
+
p.banner = "Usage: tofu [options]"
|
8
|
+
|
9
|
+
p.on("-rHOST", "--remove=HOST", "Host to remove from the ~/.tofu/known_hosts file") do |host|
|
10
|
+
Net::Tofu::Pub.db_exist?
|
11
|
+
|
12
|
+
lines = File.read(Net::Tofu::Pub::DB).split("\n")
|
13
|
+
|
14
|
+
idx = nil
|
15
|
+
lines.each_with_index do |l, i|
|
16
|
+
h = l.split[0]
|
17
|
+
if host == h
|
18
|
+
idx = i
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
lines.delete_at(idx) unless idx.nil?
|
24
|
+
|
25
|
+
f = File.open(Net::Tofu::Pub::DB, "w")
|
26
|
+
f.write lines.join("\n")
|
27
|
+
f.write "\n" unless lines.nil? || lines.empty?
|
28
|
+
|
29
|
+
exit 0
|
30
|
+
ensure
|
31
|
+
f&.close
|
32
|
+
end
|
33
|
+
|
34
|
+
p.on("-V", "--version", "Print the version then quit") do
|
35
|
+
puts "tofu v#{Net::Tofu::VERSION}"
|
36
|
+
exit 0
|
37
|
+
end
|
38
|
+
end.parse!
|
data/lib/net/tofu/error.rb
CHANGED
@@ -26,5 +26,14 @@ module Net
|
|
26
26
|
# Raised when a request contains an invalid URI.
|
27
27
|
class InvalidURIError < StandardError; end
|
28
28
|
end
|
29
|
+
|
30
|
+
# Raised if the host in a get request doesn't have a cached public key.
|
31
|
+
class UnknownHostError < StandardError; end
|
32
|
+
|
33
|
+
# Raised if the host returns an invalid certificate (usually, it means it is out of date).
|
34
|
+
class InvalidCertificateError < StandardError; end
|
35
|
+
|
36
|
+
# Raised if the host returns a different public key.
|
37
|
+
class PublicKeyMismatchError < StandardError; end
|
29
38
|
end
|
30
39
|
end
|
data/lib/net/tofu/pub.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Tofu
|
5
|
+
# Intermediate container for an X509 certificate public key.
|
6
|
+
# Manages Geminispace public certificates.
|
7
|
+
class Pub
|
8
|
+
DIR = File.expand_path("~/.tofu")
|
9
|
+
DB = File.expand_path("~/.tofu/known_hosts")
|
10
|
+
|
11
|
+
# @return [String] The host associated with the certificate public key.
|
12
|
+
attr_reader :host
|
13
|
+
|
14
|
+
# @return [String] The certificate public key associated with the host.
|
15
|
+
attr_reader :key
|
16
|
+
|
17
|
+
# @return [OpenSSL::X509::Certificate] The X509 certificate.
|
18
|
+
attr_reader :x509
|
19
|
+
|
20
|
+
# Constructor for the cert type.
|
21
|
+
# @param host [String] Host to associate with the certificate public key.
|
22
|
+
# @param data [String] Certificate public key to associate with the host.
|
23
|
+
def initialize(host, data)
|
24
|
+
@host = host
|
25
|
+
@key = data
|
26
|
+
@key = format_hosts(data) if data.start_with?("-----BEGIN CERTIFICATE-----")
|
27
|
+
@x509 = OpenSSL::X509::Certificate.new(to_x509)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Lookup a host in the known_hosts table.
|
31
|
+
# @param host [String] the hostname to lookup
|
32
|
+
# @return [Cert] An instance of this class if found, or nil if not found.
|
33
|
+
def self.lookup(host)
|
34
|
+
db_exist?
|
35
|
+
|
36
|
+
f = File.read(DB)
|
37
|
+
includes = nil
|
38
|
+
f.lines.each_with_index do |l, i|
|
39
|
+
if l.split[0].include? host
|
40
|
+
@line = i
|
41
|
+
includes = l.split[1]
|
42
|
+
break
|
43
|
+
end
|
44
|
+
end
|
45
|
+
return nil if includes.nil? || includes.empty?
|
46
|
+
|
47
|
+
new(host, includes)
|
48
|
+
end
|
49
|
+
|
50
|
+
def line
|
51
|
+
@line ||= nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Pin a certificate public key to the known_hosts file
|
55
|
+
def pin
|
56
|
+
Net::Tofu::Pub.db_exist?
|
57
|
+
|
58
|
+
f = File.open(DB, "a")
|
59
|
+
f.puts self
|
60
|
+
ensure
|
61
|
+
f&.close
|
62
|
+
end
|
63
|
+
|
64
|
+
# Conver the certificate public key to a properly formatted X509 string.
|
65
|
+
def to_x509
|
66
|
+
lines = @key.chars.each_slice(64).map(&:join)
|
67
|
+
"-----BEGIN CERTIFICATE-----\n#{lines.join("\n")}\n-----END CERTIFICATE-----"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Hosts file format.
|
71
|
+
def to_s
|
72
|
+
"#{@host} #{@key}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Compare certificate public keys
|
76
|
+
def ==(other)
|
77
|
+
@key == other.key
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if known_hosts file exists, try to create it if it doesn't.
|
81
|
+
def self.db_exist?
|
82
|
+
return true if File.exist?(DB)
|
83
|
+
|
84
|
+
FileUtils.mkdir_p(DIR) unless File.directory?(DIR)
|
85
|
+
FileUtils.touch(DB)
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Strip header, tail, and newlines from base64 encoded certificate public key.
|
92
|
+
def format_hosts(key)
|
93
|
+
key.gsub("-----BEGIN CERTIFICATE-----", "").gsub("-----END CERTIFICATE-----", "").gsub("\n", "")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/net/tofu/request.rb
CHANGED
@@ -33,7 +33,7 @@ module Net
|
|
33
33
|
# Constructor for the request type.
|
34
34
|
# @param host [String] A host string, optionally with the gemini:// scheme.
|
35
35
|
# @param port [Integer] Optional parameter to specify the server port to connect to.
|
36
|
-
def initialize(host, port: nil)
|
36
|
+
def initialize(host, port: nil, trust: false)
|
37
37
|
@uri = URI(host)
|
38
38
|
@uri.port = port unless port.nil?
|
39
39
|
|
@@ -47,7 +47,7 @@ module Net
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# Create a socket
|
50
|
-
@sock = Socket.new(@host, @port)
|
50
|
+
@sock = Socket.new(@host, @port, trust)
|
51
51
|
end
|
52
52
|
|
53
53
|
# Format the URI for sending over a socket to a Gemini server.
|
data/lib/net/tofu/socket.rb
CHANGED
@@ -7,9 +7,10 @@ module Net
|
|
7
7
|
# Constructor for the socket type.
|
8
8
|
# @param host [String] Hostname of the server to connect to.
|
9
9
|
# @param port [Integer] Server port to connect to (typically 1965).
|
10
|
-
def initialize(host, port)
|
10
|
+
def initialize(host, port, trust)
|
11
11
|
@host = host
|
12
12
|
@port = port
|
13
|
+
@trust = trust
|
13
14
|
@sock = OpenSSL::SSL::SSLSocket.open(@host, @port, context: generate_secure_context)
|
14
15
|
end
|
15
16
|
|
@@ -17,6 +18,7 @@ module Net
|
|
17
18
|
def connect
|
18
19
|
@sock.hostname = @host
|
19
20
|
@sock.connect
|
21
|
+
validate_certs
|
20
22
|
end
|
21
23
|
|
22
24
|
# Try and retrieve data from a request.
|
@@ -49,6 +51,37 @@ module Net
|
|
49
51
|
ctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF
|
50
52
|
ctx
|
51
53
|
end
|
54
|
+
|
55
|
+
# Validate a remote certificate with a local one
|
56
|
+
def validate_certs
|
57
|
+
remote = Pub.new(@host, @sock.peer_cert.to_pem)
|
58
|
+
local = Pub.lookup(@host)
|
59
|
+
|
60
|
+
raise UnknownHostError if local.nil? && !@trust
|
61
|
+
|
62
|
+
remote.pin if local.nil? && @trust
|
63
|
+
local = Pub.lookup(@host)
|
64
|
+
|
65
|
+
unless local == remote
|
66
|
+
raise PublicKeyMismatchError, <<-TXT
|
67
|
+
The remote host has a different certificate than what is cached locally.
|
68
|
+
This could be because a new certificate was issued, or because of a MITM attack.
|
69
|
+
Please verify the new public certificate, then you may run:
|
70
|
+
|
71
|
+
tofu -r #{@host}
|
72
|
+
|
73
|
+
Once you remove the old certificate public key, you will be able to add the new one.
|
74
|
+
TXT
|
75
|
+
end
|
76
|
+
|
77
|
+
validate_timestamp(remote)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate certificate timestamps
|
81
|
+
def validate_timestamp(remote)
|
82
|
+
raise InvalidCertificateError, "Certificate is not valid yet" if remote.x509.not_before > Time.now.utc
|
83
|
+
raise InvalidCertificateError, "Certificate is no longer valid" if remote.x509.not_after < Time.now.utc
|
84
|
+
end
|
52
85
|
end
|
53
86
|
end
|
54
87
|
end
|
data/lib/net/tofu/version.rb
CHANGED
data/lib/net/tofu.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "tofu/error"
|
4
|
+
require_relative "tofu/pub"
|
4
5
|
require_relative "tofu/request"
|
5
6
|
require_relative "tofu/response"
|
6
7
|
require_relative "tofu/socket"
|
7
8
|
require_relative "tofu/version"
|
8
9
|
|
10
|
+
require "fileutils"
|
9
11
|
require "openssl"
|
10
12
|
require "stringio"
|
11
13
|
require "uri/gemini"
|
@@ -13,8 +15,8 @@ require "uri/gemini"
|
|
13
15
|
module Net
|
14
16
|
# Top level module for Geminispace requests.
|
15
17
|
module Tofu
|
16
|
-
def self.get(uri, trust:
|
17
|
-
req = Request.new(uri)
|
18
|
+
def self.get(uri, trust: false)
|
19
|
+
req = Request.new(uri, trust: trust)
|
18
20
|
req.gets
|
19
21
|
|
20
22
|
return req.resp.body if req.resp.success?
|
@@ -22,8 +24,8 @@ module Net
|
|
22
24
|
req.resp.meta
|
23
25
|
end
|
24
26
|
|
25
|
-
def self.get_response(uri, trust:
|
26
|
-
req = Request.new(uri)
|
27
|
+
def self.get_response(uri, trust: false)
|
28
|
+
req = Request.new(uri, trust: trust)
|
27
29
|
req.gets
|
28
30
|
|
29
31
|
req.resp
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opentofu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rory Dudley
|
@@ -15,7 +15,8 @@ description: |2
|
|
15
15
|
the 'trust on first use' authentication scheme (https://en.wikipedia.org/wiki/Trust_on_first_use).
|
16
16
|
email:
|
17
17
|
- rory.dudley@gmail.com
|
18
|
-
executables:
|
18
|
+
executables:
|
19
|
+
- tofu
|
19
20
|
extensions: []
|
20
21
|
extra_rdoc_files: []
|
21
22
|
files:
|
@@ -24,8 +25,10 @@ files:
|
|
24
25
|
- LICENSE.txt
|
25
26
|
- README.md
|
26
27
|
- Rakefile
|
28
|
+
- exe/tofu
|
27
29
|
- lib/net/tofu.rb
|
28
30
|
- lib/net/tofu/error.rb
|
31
|
+
- lib/net/tofu/pub.rb
|
29
32
|
- lib/net/tofu/request.rb
|
30
33
|
- lib/net/tofu/response.rb
|
31
34
|
- lib/net/tofu/socket.rb
|