web_package 0.1.0 → 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 +6 -0
- data/README.md +61 -10
- data/lib/web_package/cbor.rb +5 -1
- data/lib/web_package/configuration_hash.rb +18 -0
- data/lib/web_package/inner_response.rb +26 -1
- data/lib/web_package/mice.rb +3 -36
- data/lib/web_package/middleware.rb +12 -8
- data/lib/web_package/settings.rb +17 -0
- data/lib/web_package/signed_http_exchange.rb +66 -57
- data/lib/web_package/signer.rb +8 -4
- data/lib/web_package/version.rb +1 -1
- data/lib/web_package.rb +3 -0
- data/web_package.gemspec +0 -1
- metadata +4 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39e5ffddf3e4348860d6c29e6aec54631d247c7a
|
4
|
+
data.tar.gz: c1b44063c3c6861c20875709cee60e0f2fa63277
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b962ea793e579733474c15fe721f8043c9ecd369bc66c99eace2b7a46ac65fd31088812a366a0cd71bdca31c2a33bb52e905555907aca5f58c615d9451778b22
|
7
|
+
data.tar.gz: 0f437161ae65822e028d1d9177f096007de4935dc236b0cdd56096dbf4f268e3fef68d89117e78b993541ff04f2c8b2fc34dfecce10e150f53943de61db036a6
|
data/.rubocop.yml
CHANGED
@@ -35,11 +35,17 @@ Naming/UncommunicativeMethodParamName:
|
|
35
35
|
AllowedNames:
|
36
36
|
- s
|
37
37
|
|
38
|
+
Style/ClassVars:
|
39
|
+
Enabled: false
|
40
|
+
|
38
41
|
Style/CharacterLiteral:
|
39
42
|
Enabled: false
|
40
43
|
|
41
44
|
Style/FrozenStringLiteralComment:
|
42
45
|
Enabled: false
|
43
46
|
|
47
|
+
Style/MultilineBlockChain:
|
48
|
+
Enabled: false
|
49
|
+
|
44
50
|
Style/RedundantReturn:
|
45
51
|
Enabled: false
|
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# Web Package
|
2
2
|
Ruby implementation of Signed HTTP Exchange format, allowing a browser to trust that a HTTP request-response pair was generated by the origin it claims.
|
3
3
|
|
4
|
-
|
5
4
|
## Ever thought of saving the Internet on a flash?
|
6
5
|
|
7
6
|
Easily-peasily.
|
@@ -12,27 +11,79 @@ For that we need a certificate with a special "CanSignHttpExchanges" extension,
|
|
12
11
|
|
13
12
|
Also we need an `https` cdn serving static certificate in `application/cert-chain+cbor` format. We can use `gen-certurl` tool from [here](https://github.com/WICG/webpackage/tree/master/go/signedexchange#creating-our-first-signed-exchange) to convert PEM certificate into this format, so we could than serve it from a cdn.
|
14
13
|
|
15
|
-
###
|
16
|
-
|
14
|
+
### Configuration
|
15
|
+
|
16
|
+
Several parameters can be modified via `WebPackage::Settings` to configure **WebPackage** behavior.
|
17
|
+
E.g.
|
18
|
+
```ruby
|
19
|
+
# variables can be set all at once:
|
20
|
+
WebPackage::Settings.merge! expires_in: ->(uri) { uri.path.start_with?('/news') ? 7.days : 1.day },
|
21
|
+
url_filter: ->(uri) { uri.host.start_with?('amp') },
|
22
|
+
sub_extension: '.html'
|
23
|
+
# or individually via dot-methods:
|
24
|
+
WebPackage::Settings.cert_url = 'https://my.cdn.com/cert.cbor'
|
25
|
+
```
|
26
|
+
|
27
|
+
#### headers
|
28
|
+
A `Hash`, representing html headers of SXG (outer) response.
|
29
|
+
|
30
|
+
By default three headers are set: `Content-Type`, `Cache-Control`, `X-Content-Type-Options`.
|
31
|
+
|
32
|
+
#### expires_in
|
33
|
+
An `Integer` or a `Proc` evaluating to an `Integer` or an object responding to `to_i`. It sets the lifetime of signed exchange, in seconds.
|
34
|
+
|
35
|
+
Default value is 7 days (604800 seconds), which is the maximum allowed by the standard. Please mind it when supplying your `Proc`.
|
36
|
+
|
37
|
+
#### sub_extension
|
38
|
+
A `String` or `nil`, representing an extension to use for proxying `.sxg` requests.
|
39
|
+
|
40
|
+
Default value is `nil`, which means that `.sxg` extension is just removed from the path for the rest of Rack middlewares.
|
41
|
+
|
42
|
+
#### url_filter
|
43
|
+
A `Proc`, accepting a single argument of original URI and returning boolean value. The filter determines for which paths `.sxg` formatted routes should be added.
|
44
|
+
|
45
|
+
Default value is `->(uri) { true }`, which means that all known paths are permitted and hence can be processed in SXG format using `.sxg` extension.
|
46
|
+
|
47
|
+
#### cert_url, cert_path, priv_path
|
48
|
+
|
49
|
+
All three are `String`, pointing to a certificate with which all pages are to be signed:
|
50
|
+
- `cert_url` is the url of a certificate in `application/cert-chain+cbor` format
|
51
|
+
- `cert_path` and `priv_path` are two paths pointing at `pem` file and private key file respectively.
|
52
|
+
|
53
|
+
These are the only parameters which do not have default values. An exception is raised if they are not set beforehand. Please refer below to the section of _Required variables_ on the ways to set them.
|
54
|
+
|
55
|
+
### Required variables
|
56
|
+
|
57
|
+
For smooth running **WebPackage** requires three variables to be set. It can be done either via environment or with the use of `WebPackage::Settings` object:
|
17
58
|
```bash
|
18
59
|
export SXG_CERT_URL='https://my.cdn.com/cert.cbor' \
|
19
|
-
SXG_CERT_PATH='/
|
20
|
-
SXG_PRIV_PATH='/
|
60
|
+
SXG_CERT_PATH='/path/to/cert.pem' \
|
61
|
+
SXG_PRIV_PATH='/path/to/priv.key'
|
62
|
+
```
|
63
|
+
or
|
64
|
+
```ruby
|
65
|
+
# app/initializers/web_package_init.rb
|
66
|
+
|
67
|
+
# variables can be set all at once:
|
68
|
+
WebPackage::Settings.merge! cert_url: 'https://my.cdn.com/cert.cbor',
|
69
|
+
cert_path: '/path/to/cert.pem',
|
70
|
+
priv_path: '/path/to/priv.key'
|
71
|
+
# or individually:
|
72
|
+
WebPackage::Settings.cert_url = 'https://my.cdn.com/cert.cbor'
|
21
73
|
```
|
22
|
-
Please note, that the variables are fetched during class initialization. And failing to provide valid paths will result in an exception.
|
23
74
|
|
24
75
|
### Use it as a middleware
|
25
76
|
|
26
77
|
`WebPackage::Middleware` can handle `.sxg`-format requests by wrapping the respective HTML contents into signed exchange response. For example the route `https://my.app.com/abc.sxg` will respond with signed contents for `https://my.app.com/abc`.
|
27
78
|
|
28
|
-
If you already have a Rack-based application (like Rails or Sinatra), than it is easy incorporate an SXG proxy into its middleware stack.
|
79
|
+
If you already have a Rack-based application (like Rails or Sinatra), than it is easy to incorporate an SXG proxy into its middleware stack.
|
29
80
|
|
30
81
|
#### Rails
|
31
82
|
Add the gem to your `Gemfile`:
|
32
83
|
```ruby
|
33
84
|
gem 'web_package'
|
34
85
|
```
|
35
|
-
And then
|
86
|
+
And then plug the middleware in:
|
36
87
|
```ruby
|
37
88
|
# config/application.rb
|
38
89
|
config.middleware.insert 0, 'WebPackage::Middleware'
|
@@ -108,4 +159,4 @@ Note, that the browser might spit a warning `You are using unsupported command-l
|
|
108
159
|
|
109
160
|
## License
|
110
161
|
|
111
|
-
Web
|
162
|
+
Web Package is released under the [MIT License](../master/LICENSE).
|
data/lib/web_package/cbor.rb
CHANGED
@@ -31,7 +31,11 @@ module WebPackage
|
|
31
31
|
bytes = hsh_size(input)
|
32
32
|
bytes[0] |= major_type(5)
|
33
33
|
|
34
|
-
|
34
|
+
# The sorting rules are:
|
35
|
+
# * If two keys have different lengths, the shorter one sorts earlier;
|
36
|
+
# * If two keys have the same length, the one with the lower value
|
37
|
+
# in (byte-wise) lexical order sorts earlier.
|
38
|
+
input.keys.sort_by { |key| [key.bytesize, *key.bytes] }.each do |key|
|
35
39
|
bytes.concat generate_bytes(key)
|
36
40
|
bytes.concat generate_bytes(input[key])
|
37
41
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module WebPackage
|
2
|
+
# A Hash class with configurable dot-methods (accessors).
|
3
|
+
class ConfigurationHash < Hash
|
4
|
+
def initialize(accessor_names, &block)
|
5
|
+
self.accessors = [*accessor_names]
|
6
|
+
super(&block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def accessors=(method_names)
|
10
|
+
self.class.class_eval do
|
11
|
+
method_names.map(&:to_sym).each do |method_name|
|
12
|
+
define_method(method_name) { self[method_name] }
|
13
|
+
define_method("#{method_name}=") { |value| self[method_name] = value }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,17 +1,42 @@
|
|
1
1
|
module WebPackage
|
2
2
|
# This class is a convenience to represent an original response, later to be signed and packed.
|
3
3
|
class InnerResponse
|
4
|
+
include Helpers
|
5
|
+
|
4
6
|
attr_reader :status, :headers, :body, :payload
|
5
7
|
|
6
8
|
def initialize(status, headers, body)
|
7
9
|
@status = status
|
8
|
-
@headers = headers
|
10
|
+
@headers = prepare_headers(headers)
|
9
11
|
@body = body
|
10
12
|
@payload = unrack_body
|
11
13
|
end
|
12
14
|
|
13
15
|
private
|
14
16
|
|
17
|
+
def prepare_headers(headers)
|
18
|
+
# The CBOR representation of a set of response metadata and headers is
|
19
|
+
# the CBOR ([RFC7049]) map with the following mappings:
|
20
|
+
# - The byte string ':status' to the byte string containing the
|
21
|
+
# response's 3-digit status code, and
|
22
|
+
# - For each response header field, the header field's lowercase name
|
23
|
+
# as a byte string to the header field's value as a byte string.
|
24
|
+
headers.dup.tap do |hsh|
|
25
|
+
# only lowercase keys allowed
|
26
|
+
hsh.keys.each { |key| hsh[key.to_s.downcase] = hsh.delete(key) }
|
27
|
+
|
28
|
+
# TODO: find out why we need (or do not need) Link header for the purpose of
|
29
|
+
# serving Signed Http Exchange
|
30
|
+
hsh.merge! ':status' => bin(@status),
|
31
|
+
'content-encoding' => 'mi-sha256-03',
|
32
|
+
'x-content-type-options' => 'nosniff'
|
33
|
+
|
34
|
+
# inner cache directive is deleted because
|
35
|
+
# exchange's response must be cacheable by a shared cache
|
36
|
+
hsh.delete 'cache-control'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
15
40
|
def unrack_body
|
16
41
|
payload = nil
|
17
42
|
|
data/lib/web_package/mice.rb
CHANGED
@@ -6,41 +6,8 @@ module WebPackage
|
|
6
6
|
|
7
7
|
CHUNK_SIZE = 2**14 # bytes
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
def initialize(headers, body)
|
12
|
-
@body = body.dup
|
13
|
-
@headers = headers.dup.tap do |hsh|
|
14
|
-
# only lowercase keys allowed
|
15
|
-
hsh.keys.each { |key| hsh[key.to_s.downcase] = hsh.delete(key) }
|
16
|
-
end
|
17
|
-
|
18
|
-
@encoded = false
|
19
|
-
end
|
20
|
-
|
21
|
-
def encode!
|
22
|
-
return if encoded?
|
23
|
-
|
24
|
-
@root_digest, @body = interlace_body_with_digests
|
25
|
-
@headers.merge! 'content-encoding' => 'mi-sha256-03',
|
26
|
-
'digest' => "mi-sha256-03=#{base64(@root_digest)}",
|
27
|
-
'x-content-type-options' => 'nosniff'
|
28
|
-
# TODO: find out why we need (or do not need) Link header for the purpose of
|
29
|
-
# serving Signed Http Exchange
|
30
|
-
# linkHeader, err := formatLinkHeader(metadata.Preloads)
|
31
|
-
# fetchResp.Header.Set("Link", linkHeader)
|
32
|
-
|
33
|
-
@encoded = true
|
34
|
-
end
|
35
|
-
|
36
|
-
def encoded?
|
37
|
-
@encoded
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def interlace_body_with_digests
|
43
|
-
num_parts = @body.bytesize.fdiv(CHUNK_SIZE).ceil
|
9
|
+
def encode(text)
|
10
|
+
num_parts = text.bytesize.fdiv(CHUNK_SIZE).ceil
|
44
11
|
|
45
12
|
chunks = []
|
46
13
|
proofs = []
|
@@ -49,7 +16,7 @@ module WebPackage
|
|
49
16
|
delimeter = i.zero? && "\x00" || "\x01"
|
50
17
|
ri = num_parts - i - 1
|
51
18
|
|
52
|
-
chunks << force_bin(
|
19
|
+
chunks << force_bin(text.byteslice(ri * CHUNK_SIZE, CHUNK_SIZE))
|
53
20
|
proofs << digest("#{chunks.last}#{proofs.last}#{delimeter}")
|
54
21
|
end
|
55
22
|
|
@@ -9,7 +9,13 @@ module WebPackage
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def call(env)
|
12
|
-
env
|
12
|
+
Settings.url_filter[uri(env)] ? process(env) : @app.call(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def process(env)
|
18
|
+
env[SXG_FLAG] = true if substitute_sxg_extension!(env['PATH_INFO'])
|
13
19
|
|
14
20
|
response = @app.call(env)
|
15
21
|
return response unless response[0] == 200 && env[SXG_FLAG]
|
@@ -18,22 +24,20 @@ module WebPackage
|
|
18
24
|
response[2].close if response[2].respond_to? :close
|
19
25
|
|
20
26
|
# substituting the original response with SXG
|
21
|
-
SignedHttpExchange.new(
|
27
|
+
SignedHttpExchange.new(uri(env), response).to_rack_response
|
22
28
|
end
|
23
29
|
|
24
|
-
|
25
|
-
|
26
|
-
def sxg_delete!(path)
|
30
|
+
def substitute_sxg_extension!(path)
|
27
31
|
return unless path.is_a?(String) && (i = path.rindex(SXG_EXT))
|
28
32
|
|
29
33
|
# check that extension is either the last char or followed by a slash
|
30
34
|
ch = path[i + SXG_EXT.size]
|
31
35
|
return if ch && ch != ?/
|
32
36
|
|
33
|
-
path
|
37
|
+
path[i, SXG_EXT.size] = Settings.sub_extension.to_s
|
34
38
|
end
|
35
39
|
|
36
|
-
def
|
40
|
+
def uri(env)
|
37
41
|
URI("https://#{env['HTTP_HOST'] || env['SERVER_NAME']}").tap do |u|
|
38
42
|
path = env['PATH_INFO']
|
39
43
|
port = env['SERVER_PORT']
|
@@ -42,7 +46,7 @@ module WebPackage
|
|
42
46
|
u.path = path
|
43
47
|
u.port = port if !u.port && port != '80'
|
44
48
|
u.query = query if query && !query.empty?
|
45
|
-
end
|
49
|
+
end
|
46
50
|
end
|
47
51
|
end
|
48
52
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module WebPackage
|
2
|
+
OPTIONS = %i[headers expires_in sub_extension url_filter cert_url cert_path priv_path].freeze
|
3
|
+
ENV_KEYS = Set.new(%w[SXG_CERT_URL SXG_CERT_PATH SXG_PRIV_PATH]).freeze
|
4
|
+
DEFAULTS = {
|
5
|
+
headers: { 'Content-Type' => 'application/signed-exchange;v=b3',
|
6
|
+
'Cache-Control' => 'no-transform',
|
7
|
+
'X-Content-Type-Options' => 'nosniff' },
|
8
|
+
expires_in: 60 * 60 * 24 * 7, # 7.days
|
9
|
+
sub_extension: nil, # proxy as default format (html)
|
10
|
+
url_filter: ->(_uri) { true } # all paths are permitted
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
Settings = ConfigurationHash.new(OPTIONS) do |config, key|
|
14
|
+
env_key = "SXG_#{key.upcase}"
|
15
|
+
config[key] = ENV.fetch env_key if ENV_KEYS.include? env_key
|
16
|
+
end.tap { |config| config.merge! DEFAULTS }
|
17
|
+
end
|
@@ -8,17 +8,10 @@ module WebPackage
|
|
8
8
|
# Current implementation is lazy, meaning that signing is performed upon the
|
9
9
|
# invocation of the `body` method.
|
10
10
|
class SignedHttpExchange
|
11
|
+
include Helpers
|
12
|
+
|
11
13
|
SIGNATURE_MAX_SIZE = 2**14
|
12
14
|
HEADERS_MAX_SIZE = 2**19
|
13
|
-
SXG_HEADERS = {
|
14
|
-
'Content-Type' => 'application/signed-exchange;v=b3',
|
15
|
-
'Cache-Control' => 'no-transform',
|
16
|
-
'X-Content-Type-Options' => 'nosniff'
|
17
|
-
}.freeze
|
18
|
-
CERT_URL = ENV.fetch 'SXG_CERT_URL'
|
19
|
-
CERT_PATH = ENV.fetch 'SXG_CERT_PATH'
|
20
|
-
PRIV_PATH = ENV.fetch 'SXG_PRIV_PATH'
|
21
|
-
INTEGRITY = 'digest/mi-sha256-03'.freeze
|
22
15
|
|
23
16
|
# Mock request-response pair just in case:
|
24
17
|
MOCK_URL = 'https://example.com/wow-fake-path'.freeze
|
@@ -28,38 +21,38 @@ module WebPackage
|
|
28
21
|
# url - request url (string)
|
29
22
|
# response - an array, equivalent to Rack's one: [status_code, headers, body]
|
30
23
|
def initialize(url = MOCK_URL, response = MOCK_RESP)
|
31
|
-
@uri
|
32
|
-
@url
|
33
|
-
@inner
|
24
|
+
@uri = build_uri_from url
|
25
|
+
@url = @uri.to_s
|
26
|
+
@inner = InnerResponse.new(*response)
|
27
|
+
@signer = Signer.take
|
34
28
|
|
35
|
-
@
|
36
|
-
@
|
37
|
-
@signer = Signer.new CERT_PATH, PRIV_PATH
|
29
|
+
@digest, @payload_body = MICE.new.encode @inner.payload
|
30
|
+
@inner.headers.merge! 'digest' => "mi-sha256-03=#{base64(@digest)}"
|
38
31
|
end
|
39
32
|
|
40
33
|
def headers
|
41
|
-
|
34
|
+
Settings.headers
|
42
35
|
end
|
43
36
|
|
44
37
|
# https://tools.ietf.org/html/draft-yasskin-http-origin-signed-responses-05#section-5.3
|
45
38
|
def body
|
46
39
|
return @body if @body
|
47
|
-
|
40
|
+
buffer = ''
|
48
41
|
|
49
42
|
# 1. 8 bytes consisting of the ASCII characters "sxg1" followed by 4
|
50
43
|
# 0x00 bytes, to serve as a file signature. This is redundant with
|
51
44
|
# the MIME type, and recipients that receive both MUST check that
|
52
45
|
# they match and stop parsing if they don't.
|
53
46
|
# TODO: The implementation of the final RFC MUST use the following line:
|
54
|
-
#
|
55
|
-
|
47
|
+
# buffer << "sxg1\x00\x00\x00\x00"
|
48
|
+
buffer << "sxg1-b3\x00"
|
56
49
|
|
57
50
|
# 2. 2 bytes storing a big-endian integer "fallbackUrlLength".
|
58
|
-
|
51
|
+
buffer << [@url.bytesize].pack('S>')
|
59
52
|
|
60
53
|
# 3. "fallbackUrlLength" bytes holding a "fallbackUrl", which MUST be
|
61
54
|
# an absolute URL with a scheme of "https".
|
62
|
-
|
55
|
+
buffer << @url
|
63
56
|
|
64
57
|
# 4. 3 bytes storing a big-endian integer "sigLength". If this is
|
65
58
|
# larger than 16384 (16*1024), parsing MUST fail.
|
@@ -67,26 +60,26 @@ module WebPackage
|
|
67
60
|
raise Errors::BodyEncodingError, 'Structured Signature Header length is too large: '\
|
68
61
|
"#{signature.bytesize} bytes, max: #{SIGNATURE_MAX_SIZE} bytes."
|
69
62
|
end
|
70
|
-
|
63
|
+
buffer << [signature.bytesize].pack('L>').byteslice(-3, 3)
|
71
64
|
|
72
65
|
# 5. 3 bytes storing a big-endian integer "headerLength". If this is
|
73
66
|
# larger than 524288 (512*1024), parsing MUST fail.
|
74
|
-
if
|
67
|
+
if cbor_encoded_headers.bytesize > HEADERS_MAX_SIZE
|
75
68
|
raise Errors::BodyEncodingError, 'Response Headers length is too large: '\
|
76
|
-
"#{
|
69
|
+
"#{cbor_encoded_headers.bytesize} bytes, max: #{HEADERS_MAX_SIZE} bytes."
|
77
70
|
end
|
78
|
-
|
71
|
+
buffer << [cbor_encoded_headers.bytesize].pack('L>').byteslice(-3, 3)
|
79
72
|
|
80
73
|
# 6. "sigLength" bytes holding the "Signature" header field's value
|
81
74
|
# (Section 3.1).
|
82
|
-
|
75
|
+
buffer << signature
|
83
76
|
|
84
77
|
# 7. "headerLength" bytes holding "signedHeaders", the canonical
|
85
78
|
# serialization (Section 3.4) of the CBOR representation of the
|
86
79
|
# response headers of the exchange represented by the "application/
|
87
80
|
# signed-exchange" resource (Section 3.2), excluding the
|
88
81
|
# "Signature" header field.
|
89
|
-
|
82
|
+
buffer << cbor_encoded_headers
|
90
83
|
|
91
84
|
# 8. The payload body (Section 3.3 of [RFC7230]) of the exchange
|
92
85
|
# represented by the "application/signed-exchange" resource.
|
@@ -95,7 +88,9 @@ module WebPackage
|
|
95
88
|
# exchange" header block has no effect. A "Transfer-Encoding"
|
96
89
|
# header field on the outer HTTP response that transfers this
|
97
90
|
# resource still has its normal effect.
|
98
|
-
|
91
|
+
buffer << @payload_body
|
92
|
+
|
93
|
+
@body = buffer
|
99
94
|
end
|
100
95
|
|
101
96
|
def to_rack_response
|
@@ -106,7 +101,7 @@ module WebPackage
|
|
106
101
|
|
107
102
|
def message
|
108
103
|
return @message if @message
|
109
|
-
|
104
|
+
buffer = ''
|
110
105
|
|
111
106
|
# Help in debugging "VerifyFinal failed." error, source code:
|
112
107
|
# https://github.com/chromium/chromium/blob/8f0bd6c8be04f0dd556d42820f1eec0963dfe10b/
|
@@ -124,49 +119,50 @@ module WebPackage
|
|
124
119
|
# certificate and an exchange-signing certificate.
|
125
120
|
|
126
121
|
# 1. A string that consists of octet 32 (0x20) repeated 64 times.
|
127
|
-
|
122
|
+
buffer << "\x20" * 64
|
128
123
|
|
129
124
|
# 2. A context string: the ASCII encoding of "HTTP Exchange 1".
|
130
125
|
# ... but implementations of drafts MUST NOT use it and MUST use another
|
131
126
|
# draft-specific string beginning with "HTTP Exchange 1 " instead.
|
132
127
|
# TODO: The implementation of the final RFC MUST use the following line:
|
133
|
-
#
|
134
|
-
|
128
|
+
# buffer << "HTTP Exchange 1"
|
129
|
+
buffer << 'HTTP Exchange 1 b3'
|
135
130
|
|
136
131
|
# 3. A single 0 byte which serves as a separator.
|
137
|
-
|
132
|
+
buffer << "\x00"
|
138
133
|
|
139
134
|
# 4. If "cert-sha256" is set, a byte holding the value 32
|
140
135
|
# followed by the 32 bytes of the value of "cert-sha256".
|
141
136
|
# Otherwise a 0 byte.
|
142
|
-
|
137
|
+
buffer << (@signer.cert_sha256 ? "\x20#{@signer.cert_sha256}" : "\x00")
|
143
138
|
|
144
139
|
# 5. The 8-byte big-endian encoding of the length in bytes of
|
145
140
|
# "validity-url", followed by the bytes of "validity-url".
|
146
|
-
|
147
|
-
|
141
|
+
buffer << [validity_url.bytesize].pack('Q>')
|
142
|
+
buffer << validity_url
|
148
143
|
|
149
144
|
# 6. The 8-byte big-endian encoding of "date".
|
150
|
-
|
145
|
+
buffer << [signed_at.to_i].pack('Q>')
|
151
146
|
|
152
147
|
# 7. The 8-byte big-endian encoding of "expires".
|
153
|
-
|
148
|
+
buffer << [expires_at.to_i].pack('Q>')
|
154
149
|
|
155
150
|
# 8. The 8-byte big-endian encoding of the length in bytes of
|
156
151
|
# "requestUrl", followed by the bytes of "requestUrl".
|
157
|
-
|
158
|
-
|
152
|
+
buffer << [@url.bytesize].pack('Q>')
|
153
|
+
buffer << @url
|
159
154
|
|
160
155
|
# 9. The 8-byte big-endian encoding of the length in bytes of
|
161
156
|
# "responseHeaders", followed by the bytes of
|
162
157
|
# "responseHeaders".
|
163
|
-
|
164
|
-
|
158
|
+
buffer << [cbor_encoded_headers.bytesize].pack('Q>')
|
159
|
+
buffer << cbor_encoded_headers
|
160
|
+
|
161
|
+
@message = buffer
|
165
162
|
end
|
166
163
|
|
167
|
-
def
|
168
|
-
@
|
169
|
-
@cbor.generate @mice.headers.merge(':status' => bin(@inner.status))
|
164
|
+
def cbor_encoded_headers
|
165
|
+
@cbor_encoded_headers ||= CBOR.new.generate @inner.headers
|
170
166
|
end
|
171
167
|
|
172
168
|
# returns a string representing serialized label + params
|
@@ -199,14 +195,35 @@ module WebPackage
|
|
199
195
|
# 4tb9Q==*;validity-url="https://example.com/resource.validity.msg"
|
200
196
|
@signature ||=
|
201
197
|
structured_header_for 'label', 'cert-sha256': @signer.cert_sha256.bytes,
|
202
|
-
'cert-url':
|
203
|
-
'date':
|
204
|
-
'expires':
|
205
|
-
'integrity':
|
198
|
+
'cert-url': Settings.cert_url,
|
199
|
+
'date': signed_at.to_i,
|
200
|
+
'expires': expires_at.to_i,
|
201
|
+
'integrity': 'digest/mi-sha256-03',
|
206
202
|
'sig': @signer.sign(message).bytes,
|
207
203
|
'validity-url': validity_url
|
208
204
|
end
|
209
205
|
|
206
|
+
def signed_at
|
207
|
+
@signed_at ||= Time.now
|
208
|
+
end
|
209
|
+
|
210
|
+
def expires_at
|
211
|
+
@expires_at ||= begin
|
212
|
+
lifetime = case Settings.expires_in
|
213
|
+
when Integer then Settings.expires_in
|
214
|
+
when Proc then Settings.expires_in[@uri].to_i
|
215
|
+
else raise 'Settings.expires_in is allowed to be Integer or Proc only'
|
216
|
+
end
|
217
|
+
|
218
|
+
# valid lifetime is within (0, 7.days] range
|
219
|
+
if lifetime <= 0 || lifetime > DEFAULTS[:expires_in]
|
220
|
+
raise "expires_in (#{lifetime}) is out of permitted range (0, #{DEFAULTS[:expires_in]}]"
|
221
|
+
end
|
222
|
+
|
223
|
+
signed_at + lifetime
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
210
227
|
def build_uri_from(url)
|
211
228
|
u = url.is_a?(URI) ? url : URI(url)
|
212
229
|
raise '[SignedHttpExchange] Request host is required' if u.host.nil?
|
@@ -223,13 +240,5 @@ module WebPackage
|
|
223
240
|
URI::HTTPS.build(host: @uri.host, path: no_format_path, query: @uri.query).to_s
|
224
241
|
end
|
225
242
|
end
|
226
|
-
|
227
|
-
def bin(s)
|
228
|
-
s.to_s.force_encoding Encoding::ASCII_8BIT
|
229
|
-
end
|
230
|
-
|
231
|
-
def base64(s)
|
232
|
-
Base64.strict_encode64 s
|
233
|
-
end
|
234
243
|
end
|
235
244
|
end
|
data/lib/web_package/signer.rb
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
require 'openssl'
|
2
|
+
require 'singleton'
|
2
3
|
|
3
4
|
module WebPackage
|
4
5
|
# Performs signing of a message with ECDSA.
|
5
6
|
class Signer
|
7
|
+
include Singleton
|
6
8
|
include Helpers
|
7
|
-
|
9
|
+
|
10
|
+
attr_reader :cert, :cert_url
|
11
|
+
|
12
|
+
def self.take
|
13
|
+
@@instance ||= new(Settings.cert_path, Settings.priv_path)
|
14
|
+
end
|
8
15
|
|
9
16
|
def initialize(path_to_cert, path_to_key)
|
10
17
|
@alg = OpenSSL::PKey::EC.new(File.read(path_to_key))
|
11
18
|
@cert = OpenSSL::X509::Certificate.new(File.read(path_to_cert))
|
12
|
-
|
13
|
-
@signed_at = Time.now
|
14
|
-
@expires_at = @signed_at + 60 * 60 * 24 * 7
|
15
19
|
end
|
16
20
|
|
17
21
|
def sign(message)
|
data/lib/web_package/version.rb
CHANGED
data/lib/web_package.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
require 'uri'
|
2
|
+
require 'set'
|
2
3
|
|
3
4
|
require 'web_package/errors/body_encoding_error'
|
4
5
|
require 'web_package/version'
|
5
6
|
require 'web_package/helpers'
|
7
|
+
require 'web_package/configuration_hash'
|
8
|
+
require 'web_package/settings'
|
6
9
|
require 'web_package/mice'
|
7
10
|
require 'web_package/cbor'
|
8
11
|
require 'web_package/inner_response'
|
data/web_package.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: web_package
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oleg Afanasyev
|
@@ -9,22 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-05-
|
12
|
+
date: 2019-05-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: byebug
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
requirements:
|
18
|
-
- - ">="
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: '0'
|
21
|
-
type: :development
|
22
|
-
prerelease: false
|
23
|
-
version_requirements: !ruby/object:Gem::Requirement
|
24
|
-
requirements:
|
25
|
-
- - ">="
|
26
|
-
- !ruby/object:Gem::Version
|
27
|
-
version: '0'
|
28
14
|
- !ruby/object:Gem::Dependency
|
29
15
|
name: rubocop
|
30
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,11 +39,13 @@ files:
|
|
53
39
|
- README.md
|
54
40
|
- lib/web_package.rb
|
55
41
|
- lib/web_package/cbor.rb
|
42
|
+
- lib/web_package/configuration_hash.rb
|
56
43
|
- lib/web_package/errors/body_encoding_error.rb
|
57
44
|
- lib/web_package/helpers.rb
|
58
45
|
- lib/web_package/inner_response.rb
|
59
46
|
- lib/web_package/mice.rb
|
60
47
|
- lib/web_package/middleware.rb
|
48
|
+
- lib/web_package/settings.rb
|
61
49
|
- lib/web_package/signed_http_exchange.rb
|
62
50
|
- lib/web_package/signer.rb
|
63
51
|
- lib/web_package/version.rb
|