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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3480112115fbe00503b06c0f2348ce71540476a2
4
- data.tar.gz: 302a745b6299ed41ce24477ea0e97e2028f36f1c
3
+ metadata.gz: 39e5ffddf3e4348860d6c29e6aec54631d247c7a
4
+ data.tar.gz: c1b44063c3c6861c20875709cee60e0f2fa63277
5
5
  SHA512:
6
- metadata.gz: f246d62cead117fbd4e8f8da7ccba8efaa29ee22baeebf1d7aadcbf73fe26131d58640876810ee1c6676591538c07a23f8d2894daba08348b01ef9c10293efcc
7
- data.tar.gz: 1e5f467edfb293b83b166bc1ce03d2bda57db2af45fdfd9d98ddcecff65aafb21edd879e38ef43999a5a9b81b0e8ae3959fbbc32eb8bfa7086cec5b0380b164f
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
- # web_package
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
- ### Required environment variables
16
- Having done the above-said we are now ready to assign required env vars:
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='/local/path/to/cert.pem' \
20
- SXG_PRIV_PATH='/local/path/to/priv.key'
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 add the middleware:
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 package is released under the [MIT License](../master/LICENSE).
162
+ Web Package is released under the [MIT License](../master/LICENSE).
@@ -31,7 +31,11 @@ module WebPackage
31
31
  bytes = hsh_size(input)
32
32
  bytes[0] |= major_type(5)
33
33
 
34
- input.keys.sort_by(&:bytesize).each do |key|
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
 
@@ -6,41 +6,8 @@ module WebPackage
6
6
 
7
7
  CHUNK_SIZE = 2**14 # bytes
8
8
 
9
- attr_reader :headers, :body
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(@body.byteslice(ri * CHUNK_SIZE, CHUNK_SIZE))
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[SXG_FLAG] = true if sxg_delete!(env['PATH_INFO'])
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(url(env), response).to_rack_response
27
+ SignedHttpExchange.new(uri(env), response).to_rack_response
22
28
  end
23
29
 
24
- private
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.slice! i, SXG_EXT.size
37
+ path[i, SXG_EXT.size] = Settings.sub_extension.to_s
34
38
  end
35
39
 
36
- def url(env)
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.to_s
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 = build_uri_from url
32
- @url = @uri.to_s
33
- @inner = InnerResponse.new(*response)
24
+ @uri = build_uri_from url
25
+ @url = @uri.to_s
26
+ @inner = InnerResponse.new(*response)
27
+ @signer = Signer.take
34
28
 
35
- @cbor = CBOR.new
36
- @mice = MICE.new(@inner.headers, @inner.payload).tap(&:encode!)
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
- SXG_HEADERS
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
- @body = ''
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
- # @body << "sxg1\x00\x00\x00\x00"
55
- @body << "sxg1-b3\x00"
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
- @body << [@url.bytesize].pack('S>')
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
- @body << @url
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
- @body << [signature.bytesize].pack('L>').byteslice(-3, 3)
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 encoded_mice_headers.bytesize > HEADERS_MAX_SIZE
67
+ if cbor_encoded_headers.bytesize > HEADERS_MAX_SIZE
75
68
  raise Errors::BodyEncodingError, 'Response Headers length is too large: '\
76
- "#{encoded_mice_headers.bytesize} bytes, max: #{HEADERS_MAX_SIZE} bytes."
69
+ "#{cbor_encoded_headers.bytesize} bytes, max: #{HEADERS_MAX_SIZE} bytes."
77
70
  end
78
- @body << [encoded_mice_headers.bytesize].pack('L>').byteslice(-3, 3)
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
- @body << signature
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
- @body << encoded_mice_headers
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
- @body << @mice.body
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
- @message = ''
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
- @message << "\x20" * 64
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
- # @message << "HTTP Exchange 1"
134
- @message << 'HTTP Exchange 1 b3'
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
- @message << "\x00"
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
- @message << (@signer.cert_sha256 ? "\x20#{@signer.cert_sha256}" : "\x00")
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
- @message << [validity_url.bytesize].pack('Q>')
147
- @message << validity_url
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
- @message << [@signer.signed_at.to_i].pack('Q>')
145
+ buffer << [signed_at.to_i].pack('Q>')
151
146
 
152
147
  # 7. The 8-byte big-endian encoding of "expires".
153
- @message << [@signer.expires_at.to_i].pack('Q>')
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
- @message << [@url.bytesize].pack('Q>')
158
- @message << @url
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
- @message << [encoded_mice_headers.bytesize].pack('Q>')
164
- @message << encoded_mice_headers
158
+ buffer << [cbor_encoded_headers.bytesize].pack('Q>')
159
+ buffer << cbor_encoded_headers
160
+
161
+ @message = buffer
165
162
  end
166
163
 
167
- def encoded_mice_headers
168
- @encoded_mice_headers ||=
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': CERT_URL,
203
- 'date': @signer.signed_at.to_i,
204
- 'expires': @signer.expires_at.to_i,
205
- 'integrity': 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
@@ -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
- attr_reader :signed_at, :expires_at, :cert, :integrity, :cert_url
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)
@@ -1,3 +1,3 @@
1
1
  module WebPackage
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
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
@@ -18,6 +18,5 @@ Gem::Specification.new do |s|
18
18
 
19
19
  s.required_ruby_version = '>=2.2.0'
20
20
 
21
- s.add_development_dependency 'byebug'
22
21
  s.add_development_dependency 'rubocop', '~> 0.67'
23
22
  end
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.1.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 00:00:00.000000000 Z
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