linzer 0.8.0.beta1 → 0.8.0.beta2
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/.standard.yml +1 -0
- data/CHANGELOG.md +7 -0
- data/README.md +55 -0
- data/lib/faraday/http_signature/middleware.rb +25 -4
- data/lib/linzer/common.rb +2 -2
- data/lib/linzer/helper.rb +34 -15
- data/lib/linzer/http/signature_feature.rb +15 -4
- data/lib/linzer/http/structured_field.rb +123 -26
- data/lib/linzer/http.rb +13 -7
- data/lib/linzer/message/adapter/abstract.rb +30 -17
- data/lib/linzer/message/adapter/generic/request.rb +1 -0
- data/lib/linzer/message/field/parser.rb +5 -5
- data/lib/linzer/message/field.rb +4 -4
- data/lib/linzer/message/overlay.rb +143 -0
- data/lib/linzer/message.rb +18 -0
- data/lib/linzer/signature/context.rb +80 -0
- data/lib/linzer/signature/profile/base.rb +43 -0
- data/lib/linzer/signature/profile/example.rb +39 -0
- data/lib/linzer/signature/profile/web_bot_auth.rb +201 -0
- data/lib/linzer/signature/profile.rb +70 -0
- data/lib/linzer/signature.rb +29 -39
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +5 -2
- metadata +7 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Message
|
|
5
|
+
# Overlay provides a signing-time augmentation layer for HTTP headers.
|
|
6
|
+
#
|
|
7
|
+
# It allows additional headers to be introduced during HTTP Message
|
|
8
|
+
# Signature generation without mutating the underlying HTTP message.
|
|
9
|
+
#
|
|
10
|
+
# IMPORTANT SEMANTICS
|
|
11
|
+
#
|
|
12
|
+
# Overlay affects ONLY HTTP header resolution.
|
|
13
|
+
#
|
|
14
|
+
# It MUST NOT influence derived HTTP Message Signature components such as:
|
|
15
|
+
# - @method
|
|
16
|
+
# - @authority
|
|
17
|
+
# - @target-uri
|
|
18
|
+
#
|
|
19
|
+
# These values are always computed from the underlying HTTP message.
|
|
20
|
+
#
|
|
21
|
+
# ---
|
|
22
|
+
#
|
|
23
|
+
# Resolution rules:
|
|
24
|
+
#
|
|
25
|
+
# Header lookup:
|
|
26
|
+
# 1. Underlying message headers
|
|
27
|
+
# 2. Overlay headers (fallback only)
|
|
28
|
+
#
|
|
29
|
+
# Derived components:
|
|
30
|
+
# Always resolved from the underlying message only
|
|
31
|
+
#
|
|
32
|
+
# ---
|
|
33
|
+
#
|
|
34
|
+
# Purpose:
|
|
35
|
+
#
|
|
36
|
+
# This class exists to support signing-time augmentation of header values
|
|
37
|
+
# (e.g., injected or synthesized headers required by signing profiles)
|
|
38
|
+
# without altering the canonical representation of the HTTP message.
|
|
39
|
+
#
|
|
40
|
+
# This is NOT a full message override layer.
|
|
41
|
+
# It is a header-only augmentation mechanism used during signing.
|
|
42
|
+
#
|
|
43
|
+
# DESIGN NOTE:
|
|
44
|
+
# Overlay does not implement full HTTP message semantics.
|
|
45
|
+
# It only participates in header resolution for signing-time evaluation.
|
|
46
|
+
class Overlay
|
|
47
|
+
# Creates a new overlay message.
|
|
48
|
+
#
|
|
49
|
+
# @param message [Linzer::Message]
|
|
50
|
+
# The underlying message to wrap
|
|
51
|
+
# @param overlay_headers [#to_h, Hash]
|
|
52
|
+
# Additional headers to overlay onto the message
|
|
53
|
+
# A hash-like object containing HTTP headers to use as an overlay layer.
|
|
54
|
+
# Keys and values must be compatible with Net::HTTP header semantics.
|
|
55
|
+
def initialize(message, overlay_headers)
|
|
56
|
+
@message = message
|
|
57
|
+
@overlay_headers = overlay_headers
|
|
58
|
+
|
|
59
|
+
# Internal adapter-backed overlay used to reuse Linzer's field
|
|
60
|
+
# resolution logic for header/field evaluation.
|
|
61
|
+
@overlay = build_overlay_message(overlay_headers)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns a header value from the signing-time resolution view.
|
|
65
|
+
#
|
|
66
|
+
# Overlay headers are only used if the underlying message does not
|
|
67
|
+
# provide a value for the requested header.
|
|
68
|
+
#
|
|
69
|
+
# Derived components (e.g. @authority, @target-uri) are not affected.
|
|
70
|
+
#
|
|
71
|
+
# @param name [String]
|
|
72
|
+
# @return [String, nil]
|
|
73
|
+
def header(name)
|
|
74
|
+
@message.header(name) || @overlay.header(name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns true if the field can be resolved from:
|
|
78
|
+
#
|
|
79
|
+
# - the underlying HTTP message (including derived fields), or
|
|
80
|
+
# - overlay headers (header fields only)
|
|
81
|
+
#
|
|
82
|
+
# NOTE:
|
|
83
|
+
# Overlay headers do not participate in derived component resolution
|
|
84
|
+
# (e.g. @method, @target-uri, @authority).
|
|
85
|
+
#
|
|
86
|
+
# @param field [Linzer::FieldId]
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def field?(field)
|
|
89
|
+
@message.field?(field) || (!field.derived? && @overlay.field?(field))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Attaches signature headers to the underlying HTTP message.
|
|
93
|
+
#
|
|
94
|
+
# Overlay headers are applied only as HTTP headers at attachment time.
|
|
95
|
+
# They do not affect derived HTTP Message Signature components.
|
|
96
|
+
#
|
|
97
|
+
# @param signature [Linzer::Signature] The signature to attach
|
|
98
|
+
# @return [Object]
|
|
99
|
+
# The underlying message returned by Linzer::Message#attach!
|
|
100
|
+
def attach!(signature)
|
|
101
|
+
@message.attach!(signature, additional_headers: @overlay_headers.to_h)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Retrieves a resolved header or field value from the signing-time view.
|
|
105
|
+
#
|
|
106
|
+
# Resolution order:
|
|
107
|
+
#
|
|
108
|
+
# 1. Underlying HTTP message
|
|
109
|
+
# 2. Overlay headers (fallback only)
|
|
110
|
+
#
|
|
111
|
+
# IMPORTANT:
|
|
112
|
+
# Overlay values are ONLY used when the underlying message does not
|
|
113
|
+
# provide a value. They do not override existing message values.
|
|
114
|
+
#
|
|
115
|
+
# Derived HTTP Message Signature components (e.g. @method,
|
|
116
|
+
# @target-uri, @authority) are always resolved exclusively from the
|
|
117
|
+
# underlying message and are never influenced by overlay headers.
|
|
118
|
+
#
|
|
119
|
+
# @param name [Linzer::FieldId]
|
|
120
|
+
# @return [Object, nil]
|
|
121
|
+
def [](name)
|
|
122
|
+
value = @message[name]
|
|
123
|
+
return value unless value.nil?
|
|
124
|
+
|
|
125
|
+
return nil if Linzer::FieldId.new(field_name: name).derived?
|
|
126
|
+
@overlay[name]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Builds a synthetic Net::HTTP request used solely to reuse Linzer's
|
|
132
|
+
# Generic::Request field resolution logic.
|
|
133
|
+
#
|
|
134
|
+
# The URI is a placeholder because only header and field resolution
|
|
135
|
+
# behavior is required; no network request is performed.
|
|
136
|
+
def build_overlay_message(headers)
|
|
137
|
+
request = Net::HTTP::Get.new(URI("https://example.invalid/"))
|
|
138
|
+
request.initialize_http_header(headers.to_h)
|
|
139
|
+
Adapter::Generic::Request.new(request)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/linzer/message.rb
CHANGED
|
@@ -113,6 +113,24 @@ module Linzer
|
|
|
113
113
|
# message.attach!(signature)
|
|
114
114
|
def_delegators :@adapter, :attach!
|
|
115
115
|
|
|
116
|
+
# Returns a new message wrapper with additional or overridden headers.
|
|
117
|
+
#
|
|
118
|
+
# The returned message preserves the original message contents while
|
|
119
|
+
# overlaying the provided headers for component resolution and
|
|
120
|
+
# signature generation.
|
|
121
|
+
#
|
|
122
|
+
# @param headers [#to_h] Headers to overlay onto the message
|
|
123
|
+
# @return [Linzer::Message::Overlay] A message wrapper using the
|
|
124
|
+
# merged header set
|
|
125
|
+
#
|
|
126
|
+
# @example Add related signature headers without mutating the message
|
|
127
|
+
# signed_message = message.with_headers(
|
|
128
|
+
# "signature-agent" => "https://example.org/automated-agent"
|
|
129
|
+
# )
|
|
130
|
+
def with_headers(overlay_headers)
|
|
131
|
+
Overlay.new(self, overlay_headers)
|
|
132
|
+
end
|
|
133
|
+
|
|
116
134
|
class << self
|
|
117
135
|
# Registers a custom adapter for an HTTP message class.
|
|
118
136
|
#
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Signature
|
|
5
|
+
# A mutable context object used during signature generation.
|
|
6
|
+
#
|
|
7
|
+
# The context represents all state required to produce a signature,
|
|
8
|
+
# including:
|
|
9
|
+
#
|
|
10
|
+
# - the HTTP message being signed
|
|
11
|
+
# - the signing key
|
|
12
|
+
# - covered signature components
|
|
13
|
+
# - signature parameters
|
|
14
|
+
# - optional overlay headers introduced by signing profiles
|
|
15
|
+
#
|
|
16
|
+
# Profiles may mutate this context before or during signing in order
|
|
17
|
+
# to influence the final signature output (e.g., adding headers,
|
|
18
|
+
# modifying components, or adjusting parameters).
|
|
19
|
+
#
|
|
20
|
+
# This object is intentionally mutable and is not thread-safe.
|
|
21
|
+
#
|
|
22
|
+
# @attr_reader [Linzer::Message] message
|
|
23
|
+
# the HTTP message being signed
|
|
24
|
+
#
|
|
25
|
+
# @attr_reader [Object] key
|
|
26
|
+
# the signing key used to generate the signature
|
|
27
|
+
#
|
|
28
|
+
# @attr_reader [Array<String>] components
|
|
29
|
+
# list of covered signature components
|
|
30
|
+
#
|
|
31
|
+
# @attr_reader [Hash] params
|
|
32
|
+
# signature parameters (may include :label if provided)
|
|
33
|
+
#
|
|
34
|
+
# @attr_reader [Hash] overlay_headers
|
|
35
|
+
# Overlay headers are merged into the message view during signature
|
|
36
|
+
# computation but do not mutate the underlying message.
|
|
37
|
+
class Context
|
|
38
|
+
# Creates a new signing context.
|
|
39
|
+
#
|
|
40
|
+
# @param message [Linzer::Message]
|
|
41
|
+
# The HTTP message being signed
|
|
42
|
+
#
|
|
43
|
+
# @param key [Linzer::Key]
|
|
44
|
+
# The signing key used to generate the signature
|
|
45
|
+
#
|
|
46
|
+
# @param label [String, nil]
|
|
47
|
+
# Optional signature label. If provided, it is merged into params
|
|
48
|
+
# as `:label`.
|
|
49
|
+
#
|
|
50
|
+
# @param components [Array<String>]
|
|
51
|
+
# The list of HTTP components covered by the signature
|
|
52
|
+
#
|
|
53
|
+
# @param params [Hash]
|
|
54
|
+
# Signature parameters passed to the signing algorithm
|
|
55
|
+
def initialize(message:, key:, label:, components:, params:)
|
|
56
|
+
@message = message
|
|
57
|
+
@key = key
|
|
58
|
+
@components = components.dup
|
|
59
|
+
@params = (label ? params.merge(label: label) : params).dup
|
|
60
|
+
@overlay_headers = {}
|
|
61
|
+
end
|
|
62
|
+
attr_reader :key, :components, :params, :overlay_headers
|
|
63
|
+
|
|
64
|
+
# Returns a message view that includes any overlay headers.
|
|
65
|
+
#
|
|
66
|
+
# The returned object is cached after first construction.
|
|
67
|
+
#
|
|
68
|
+
# Overlay headers are applied lazily and only affect the derived
|
|
69
|
+
# signing view; the original message remains unchanged.
|
|
70
|
+
#
|
|
71
|
+
# @return [Linzer::Message]
|
|
72
|
+
def message
|
|
73
|
+
return @overlay_message if defined?(@overlay_message)
|
|
74
|
+
return @message if @overlay_headers.empty?
|
|
75
|
+
|
|
76
|
+
@overlay_message = @message.with_headers(overlay_headers)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Signature
|
|
5
|
+
module Profile
|
|
6
|
+
# Base class for all signing profiles.
|
|
7
|
+
#
|
|
8
|
+
# A signing profile encapsulates policy logic that can modify a
|
|
9
|
+
# {Linzer::Signature::Context} before signature generation.
|
|
10
|
+
#
|
|
11
|
+
# Subclasses are expected to implement {#apply}.
|
|
12
|
+
#
|
|
13
|
+
# ## Lifecycle
|
|
14
|
+
#
|
|
15
|
+
# 1. Context is created
|
|
16
|
+
# 2. Profile is resolved via {.resolve}
|
|
17
|
+
# 3. {#apply} is invoked with the signing context
|
|
18
|
+
# 4. Context is used to generate signature
|
|
19
|
+
#
|
|
20
|
+
# @abstract
|
|
21
|
+
class Base
|
|
22
|
+
# Applies the profile to a signing context.
|
|
23
|
+
#
|
|
24
|
+
# Implementations may:
|
|
25
|
+
#
|
|
26
|
+
# - modify context parameters
|
|
27
|
+
# - inject overlay headers
|
|
28
|
+
# - adjust covered components
|
|
29
|
+
#
|
|
30
|
+
# @param ctx [Linzer::Signature::Context]
|
|
31
|
+
# The mutable signing context
|
|
32
|
+
#
|
|
33
|
+
# @return [void]
|
|
34
|
+
#
|
|
35
|
+
# @raise [Linzer::Error]
|
|
36
|
+
# If the subclass does not implement this method
|
|
37
|
+
def apply(ctx)
|
|
38
|
+
raise Error, "Sub-classes are required to implement this method!"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Signature
|
|
5
|
+
module Profile
|
|
6
|
+
# Example no-op signing profile.
|
|
7
|
+
#
|
|
8
|
+
# This profile exists solely for documentation and testing purposes.
|
|
9
|
+
# It does not modify the signing context in any way.
|
|
10
|
+
#
|
|
11
|
+
# It demonstrates the expected structure of a signing profile:
|
|
12
|
+
#
|
|
13
|
+
# - initializer receives configuration parameters
|
|
14
|
+
# - {#apply} mutates a {Linzer::Signature::Context}
|
|
15
|
+
#
|
|
16
|
+
# This profile is safe to use but has no effect on signature output.
|
|
17
|
+
class Example < Base
|
|
18
|
+
# Creates a new example profile instance.
|
|
19
|
+
#
|
|
20
|
+
# @param foo [Object] example configuration parameter (unused)
|
|
21
|
+
# @param bar [Object] example configuration parameter (unused)
|
|
22
|
+
def initialize(foo:, bar:)
|
|
23
|
+
@foo = foo
|
|
24
|
+
@bar = bar
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Applies this profile to the signing context.
|
|
28
|
+
#
|
|
29
|
+
# This implementation intentionally performs no modifications.
|
|
30
|
+
#
|
|
31
|
+
# @param ctx [Linzer::Signature::Context]
|
|
32
|
+
# @return [void]
|
|
33
|
+
def apply(ctx)
|
|
34
|
+
# no-op
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Signature
|
|
5
|
+
module Profile
|
|
6
|
+
# Web Bot Auth signing profile implementation.
|
|
7
|
+
#
|
|
8
|
+
# This profile applies the behavior defined in the Web Bot Auth
|
|
9
|
+
# HTTP Message Signatures draft specification.
|
|
10
|
+
#
|
|
11
|
+
# It mutates a signing context to ensure compliance with the
|
|
12
|
+
# spec requirements, including:
|
|
13
|
+
#
|
|
14
|
+
# - selection of required signature components
|
|
15
|
+
# - generation of nonce values
|
|
16
|
+
# - enforcement of Web Bot Auth signature parameters
|
|
17
|
+
# - optional Signature-Agent header injection
|
|
18
|
+
#
|
|
19
|
+
# ## Lifecycle
|
|
20
|
+
#
|
|
21
|
+
# 1. Context is created
|
|
22
|
+
# 2. Profile is resolved
|
|
23
|
+
# 3. {#apply} mutates signing context
|
|
24
|
+
# 4. signature is generated using modified context
|
|
25
|
+
#
|
|
26
|
+
# @see https://datatracker.ietf.org/wg/webbotauth/documents/
|
|
27
|
+
class WebBotAuth < Base
|
|
28
|
+
# Creates a new Web Bot Auth signing profile.
|
|
29
|
+
#
|
|
30
|
+
# @param params [Symbol, nil]
|
|
31
|
+
# Controls default Web Bot Auth signature parameters.
|
|
32
|
+
#
|
|
33
|
+
# - :recommended → apply Web Bot Auth recommended defaults
|
|
34
|
+
# - nil → do not modify signature parameters
|
|
35
|
+
#
|
|
36
|
+
# @param nonce [Symbol, nil]
|
|
37
|
+
# Controls nonce generation behavior.
|
|
38
|
+
#
|
|
39
|
+
# - :generate → inject a cryptographically random nonce
|
|
40
|
+
# - nil → no nonce is added
|
|
41
|
+
#
|
|
42
|
+
# @param agent [String, nil]
|
|
43
|
+
# Optional Signature-Agent identifier URI.
|
|
44
|
+
#
|
|
45
|
+
# When provided, a structured Signature-Agent header is injected
|
|
46
|
+
# and included as a covered signature component.
|
|
47
|
+
def initialize(params: :recommended, nonce: :generate, agent: nil)
|
|
48
|
+
@params = params
|
|
49
|
+
@nonce = nonce
|
|
50
|
+
@agent = agent
|
|
51
|
+
freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :params, :nonce, :agent
|
|
55
|
+
|
|
56
|
+
SIGNATURE_AGENT = "signature-agent"
|
|
57
|
+
private_constant :SIGNATURE_AGENT
|
|
58
|
+
|
|
59
|
+
REQUIRED_AUTH_COMPONENTS = %w[@authority @target-uri].freeze
|
|
60
|
+
|
|
61
|
+
# Applies the Web Bot Auth profile to a signing context.
|
|
62
|
+
#
|
|
63
|
+
# This method mutates:
|
|
64
|
+
# - signature parameters (ctx.params)
|
|
65
|
+
# - covered components (ctx.components)
|
|
66
|
+
# - overlay headers (ctx.overlay_headers)
|
|
67
|
+
#
|
|
68
|
+
# @param ctx [Linzer::Signature::Context]
|
|
69
|
+
# Mutable signing context
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
72
|
+
# @raise [Linzer::Error]
|
|
73
|
+
# If key or message are incompatible with Web Bot Auth rules
|
|
74
|
+
def apply(ctx)
|
|
75
|
+
validate ctx.key, ctx.message
|
|
76
|
+
|
|
77
|
+
if @params == :recommended
|
|
78
|
+
set_params!(ctx.key, ctx.components, ctx.params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
ctx.params[:nonce] = generate_nonce if @nonce == :generate
|
|
82
|
+
|
|
83
|
+
if @agent
|
|
84
|
+
set_agent!(
|
|
85
|
+
@agent,
|
|
86
|
+
ctx.params[:label],
|
|
87
|
+
ctx.message,
|
|
88
|
+
ctx.components,
|
|
89
|
+
ctx.overlay_headers
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns a default Web Bot Auth profile instance.
|
|
95
|
+
#
|
|
96
|
+
# This represents the standard recommended configuration:
|
|
97
|
+
#
|
|
98
|
+
# - recommended signature parameters enabled
|
|
99
|
+
# - nonce generation enabled
|
|
100
|
+
#
|
|
101
|
+
# @return [WebBotAuth]
|
|
102
|
+
def self.default
|
|
103
|
+
new(params: :recommended, nonce: :generate)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Applies Web Bot Auth recommended signature parameter rules.
|
|
109
|
+
#
|
|
110
|
+
# This ensures compliance with Web Bot Auth requirements:
|
|
111
|
+
#
|
|
112
|
+
# - At least one of @authority or @target-uri must be covered
|
|
113
|
+
# - expires is set to a default lifetime if not provided
|
|
114
|
+
# - tag is set to "web-bot-auth"
|
|
115
|
+
# - keyid is derived from the signing key fingerprint
|
|
116
|
+
#
|
|
117
|
+
# @param key [Linzer::JWS::Key]
|
|
118
|
+
# @param components [Array<String>]
|
|
119
|
+
# @param params [Hash]
|
|
120
|
+
# @return [void]
|
|
121
|
+
def set_params!(key, components, params)
|
|
122
|
+
# 4.2. Generating HTTP Message Signature
|
|
123
|
+
#
|
|
124
|
+
# Agents MUST include at least one of the following components:
|
|
125
|
+
# - @authority
|
|
126
|
+
# - @target-uri
|
|
127
|
+
#
|
|
128
|
+
if (components & REQUIRED_AUTH_COMPONENTS).empty?
|
|
129
|
+
components << REQUIRED_AUTH_COMPONENTS.sample
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Agents MUST include the following @signature-params:
|
|
133
|
+
# - created
|
|
134
|
+
# - expires
|
|
135
|
+
# - keyid MUST be a base64url JWK SHA-256 Thumbprint
|
|
136
|
+
# - tag MUST be web-bot-auth
|
|
137
|
+
#
|
|
138
|
+
# options[:created] is set by default by linzer at signature creation time
|
|
139
|
+
#
|
|
140
|
+
params[:expires] ||= Time.now.to_i + 3600
|
|
141
|
+
params[:tag] ||= "web-bot-auth"
|
|
142
|
+
params[:keyid] ||= key.material.key_digest
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Injects and signs the Signature-Agent header.
|
|
146
|
+
#
|
|
147
|
+
# The header is only added if:
|
|
148
|
+
# - it is not already present, OR
|
|
149
|
+
# - its value differs from the configured agent
|
|
150
|
+
#
|
|
151
|
+
# When added:
|
|
152
|
+
# - a structured Signature-Agent header is written into overlay headers
|
|
153
|
+
# - the corresponding structured field is added to covered components
|
|
154
|
+
#
|
|
155
|
+
# @param agent [String]
|
|
156
|
+
# @param label [String]
|
|
157
|
+
# @param message [Linzer::Message]
|
|
158
|
+
# @param components [Array<String>]
|
|
159
|
+
# @param overlay_headers [Hash]
|
|
160
|
+
# @return [void]
|
|
161
|
+
# @raise [Linzer::Error]
|
|
162
|
+
# If the header cannot be serialized as a structured field
|
|
163
|
+
def set_agent!(agent, label, message, components, overlay_headers)
|
|
164
|
+
if message[SIGNATURE_AGENT] != agent
|
|
165
|
+
overlay_headers[SIGNATURE_AGENT] =
|
|
166
|
+
HTTP::StructuredField.serialize_dictionary(label => agent)
|
|
167
|
+
|
|
168
|
+
field = HTTP::StructuredField::Item.new(SIGNATURE_AGENT, key: label)
|
|
169
|
+
serialized_field = HTTP::StructuredField.serialize(field)
|
|
170
|
+
if !components.include?(serialized_field)
|
|
171
|
+
components << serialized_field
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
rescue Error => ex
|
|
175
|
+
raise Error, "Invalid #{SIGNATURE_AGENT} header value!", cause: ex
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Validates that the context is compatible with Web Bot Auth.
|
|
179
|
+
#
|
|
180
|
+
# @param key [Object]
|
|
181
|
+
# @param message [Linzer::Message]
|
|
182
|
+
# @return [void]
|
|
183
|
+
# @raise [Linzer::Error]
|
|
184
|
+
def validate(key, message)
|
|
185
|
+
raise Error, "Unsupported/invalid key!" unless key.is_a?(Linzer::JWS::Key)
|
|
186
|
+
raise Error, "Web Bot Auth is defined only for requests!" unless message.request?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Generates a cryptographically random nonce.
|
|
190
|
+
#
|
|
191
|
+
# The nonce is URL-safe and suitable for inclusion in HTTP signature
|
|
192
|
+
# parameters.
|
|
193
|
+
#
|
|
194
|
+
# @return [String]
|
|
195
|
+
def generate_nonce
|
|
196
|
+
SecureRandom.urlsafe_base64(64)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "profile/base"
|
|
4
|
+
require_relative "profile/web_bot_auth"
|
|
5
|
+
|
|
6
|
+
module Linzer
|
|
7
|
+
class Signature
|
|
8
|
+
# A signing profile defines optional behavior that can modify a signing
|
|
9
|
+
# context prior to HTTP Message Signature generation.
|
|
10
|
+
#
|
|
11
|
+
# Profiles are used to encapsulate domain-specific signing rules such as:
|
|
12
|
+
#
|
|
13
|
+
# - default covered components
|
|
14
|
+
# - parameter enrichment
|
|
15
|
+
# - contextual header injection
|
|
16
|
+
# - policy-based adjustments to signing behavior
|
|
17
|
+
#
|
|
18
|
+
# Profiles are applied *before signature computation* and may mutate
|
|
19
|
+
# {Linzer::Signature::Context}.
|
|
20
|
+
module Profile
|
|
21
|
+
# Resolves a signing profile from a symbolic or object-based reference.
|
|
22
|
+
#
|
|
23
|
+
# This allows callers to pass either:
|
|
24
|
+
#
|
|
25
|
+
# - +nil+ (no profile)
|
|
26
|
+
# - an already constructed profile instance
|
|
27
|
+
# - a symbolic identifier for a registered profile
|
|
28
|
+
#
|
|
29
|
+
# @param profile [Symbol, Profile::Base, nil]
|
|
30
|
+
# The profile identifier or instance to resolve
|
|
31
|
+
#
|
|
32
|
+
# @return [Profile::Base, nil]
|
|
33
|
+
# A resolved profile instance, or +nil+ if no profile is used
|
|
34
|
+
#
|
|
35
|
+
# @raise [Linzer::Error]
|
|
36
|
+
# If the profile symbol is unknown or unsupported
|
|
37
|
+
def self.resolve(profile)
|
|
38
|
+
unsupported = "Unknown/unsupported signing profile!"
|
|
39
|
+
|
|
40
|
+
case profile
|
|
41
|
+
when NilClass, Profile::Base
|
|
42
|
+
profile
|
|
43
|
+
when Symbol
|
|
44
|
+
case profile
|
|
45
|
+
when :web_bot_auth
|
|
46
|
+
WebBotAuth.default
|
|
47
|
+
else
|
|
48
|
+
raise Error, unsupported
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
raise Error, unsupported
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Convenience constructor for a Web Bot Auth signing profile.
|
|
56
|
+
#
|
|
57
|
+
# This is a helper for constructing a profile instance directly
|
|
58
|
+
# without using {Profile.resolve}.
|
|
59
|
+
#
|
|
60
|
+
# @param options [Hash]
|
|
61
|
+
# Options passed through to WebBotAuth initializer
|
|
62
|
+
#
|
|
63
|
+
# @return [Profile::WebBotAuth]
|
|
64
|
+
# A configured WebBotAuth profile instance
|
|
65
|
+
def self.web_bot_auth(**options)
|
|
66
|
+
WebBotAuth.new(**options)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|