Class: Linzer::Signature

Inherits:
Object
  • Object
show all
Defined in:
lib/linzer/signature.rb,
lib/linzer/signature/context.rb,
lib/linzer/signature/profile.rb,
lib/linzer/signature/profile/base.rb,
lib/linzer/signature/profile/example.rb,
lib/linzer/signature/profile/web_bot_auth.rb

Overview

Represents an HTTP message signature as defined in RFC 9421.

A Signature encapsulates:

  • The raw signature bytes

  • The covered components (fields included in the signature)

  • The signature parameters (created, keyid, etc.)

  • The signature label (for identifying multiple signatures)

Signatures are immutable once created. Use Signature.build to create instances from HTTP headers, or receive them from Linzer::Signer.sign.

Examples:

Building a signature from HTTP headers

headers = {
  "signature-input" => 'sig1=("@method" "@path");created=1618884473',
  "signature" => "sig1=:base64encodedvalue...:"
}
signature = Linzer::Signature.build(headers)

Attaching a signature to a request

signature = Linzer.sign(key, message, components)
signature.to_h.each { |name, value| request[name] = value }

See Also:

Defined Under Namespace

Modules: Profile Classes: Context

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(metadata, value, label, parameters = {}, parsed_items: nil, headers: nil) ⇒ Signature

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Use build or from_components to create Signature instances.



30
31
32
33
34
35
36
37
38
# File 'lib/linzer/signature.rb', line 30

def initialize(, value, label, parameters = {}, parsed_items: nil, headers: nil)
  @metadata     = .clone.freeze
  @value        = value.clone.freeze
  @parameters   = parameters.clone.freeze
  @label        = label.clone.freeze
  @parsed_items = parsed_items&.freeze
  @headers      = headers&.freeze
  freeze
end

Instance Attribute Details

#labelObject (readonly)

Returns the value of attribute label.



56
57
58
# File 'lib/linzer/signature.rb', line 56

def label
  @label
end

#metadataObject (readonly) Also known as: serialized_components

Returns the value of attribute metadata.



43
44
45
# File 'lib/linzer/signature.rb', line 43

def 
  @metadata
end

#parametersObject (readonly)

Returns the value of attribute parameters.



52
53
54
# File 'lib/linzer/signature.rb', line 52

def parameters
  @parameters
end

#valueObject (readonly) Also known as: bytes

Returns the value of attribute value.



47
48
49
# File 'lib/linzer/signature.rb', line 47

def value
  @value
end

Class Method Details

.build(headers, options = {}) ⇒ Signature

Builds a Signature from HTTP headers.

Parses the ‘signature` and `signature-input` headers according to RFC 9421 and RFC 8941 (Structured Field Values).

Examples:

Building from request headers

headers = {
  "signature-input" => 'sig1=("@method");created=1618884473',
  "signature" => "sig1=:HIbjHC5rS0BYaa9v4QfD4193TORw7u9..=:"
}
signature = Linzer::Signature.build(headers)

Selecting a specific signature by label

signature = Linzer::Signature.build(headers, label: "sig2")

Parameters:

  • headers (Hash{String => String})

    HTTP headers containing ‘signature` and `signature-input` fields. Keys are case-insensitive.

  • options (Hash) (defaults to: {})

    Build options

Options Hash (options):

  • :label (String)

    The signature label to extract when multiple signatures are present. If not specified and multiple signatures exist, an error is raised.

Returns:

Raises:

  • (Error)

    If headers are nil or empty

  • (Error)

    If required signature headers are missing

  • (Error)

    If multiple signatures exist and no label is specified

  • (Error)

    If the specified label is not found

  • (Error)

    If the headers cannot be parsed as structured fields



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/linzer/signature.rb', line 234

def build(headers, options = {})
  basic_validate headers
  headers.transform_keys!(&:downcase)
  headers.transform_values! { |v| v.encode(Encoding::ASCII) }
  validate headers

  input = HTTP::StructuredField.parse_dictionary(
    headers["signature-input"],
    field_name: "signature-input"
  )

  reject_multiple_signatures if input.size > 1 && options[:label].nil?
  label = options[:label] || input.keys.first

  raw_signature = extract_raw_signature(headers["signature"], label)

  fail_due_invalid_components unless input[label].value.respond_to?(:each)

  parsed_items = input[label].value
  components = serialize_parsed_items(parsed_items)
  parameters = input[label].parameters

  new(components, raw_signature, label, parameters, parsed_items: parsed_items)
end

.from_components(components:, raw_signature:, label:, parameters:, parsed_items:, headers:) ⇒ Signature

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a Signature directly from its constituent parts.

This avoids the serialize-then-parse round-trip when the caller (e.g. Linzer::Signer.sign) already has all the data.

Parameters:

  • components (Array<String>)

    Serialized component identifiers

  • raw_signature (String)

    The raw signature bytes

  • label (String)

    The signature label

  • parameters (Hash)

    Signature parameters (symbol keys)

  • parsed_items (Array<Starry::Item>)

    Pre-parsed component items

  • headers (Hash)

    Pre-serialized header strings

Returns:



196
197
198
199
200
201
202
203
# File 'lib/linzer/signature.rb', line 196

def from_components(components:, raw_signature:, label:, parameters:, parsed_items:, headers:)
  # Signature stores parameters with string keys (as produced by Starry
  # parsing). Convert symbol keys from Signer to match.
  string_params = {}
  parameters.each { |k, v| string_params[k.to_s] = v }
  new(components, raw_signature, label, string_params,
      parsed_items: parsed_items, headers: headers)
end

Instance Method Details

#componentsArray<String>

Returns the deserialized component identifiers.

Unlike #serialized_components, this returns the components in a more human-readable form.

Returns:

  • (Array<String>)

    Component identifiers (e.g., ‘[“@method”, “content-type”]`)



75
76
77
# File 'lib/linzer/signature.rb', line 75

def components
  FieldId.deserialize_components(serialized_components)
end

#createdInteger?

Returns the signature creation timestamp.

Returns:

  • (Integer, nil)

    Unix timestamp when the signature was created, or nil if the ‘created` parameter is not present

Raises:

  • (Error)

    If the ‘created` parameter exists but is not an integer



98
99
100
101
102
103
# File 'lib/linzer/signature.rb', line 98

def created
  Integer(parameters["created"])
rescue
  return nil if parameters["created"].nil?
  raise Error.new "Signature has a non-integer `created` parameter"
end

#expired?Boolean

Checks if the signature has expired based on the ‘expires` parameter.

If the ‘expires` parameter is not present, the signature is considered not expired (returns false). If the parameter is present but not a valid integer, an error is raised.

Examples:

Check if a signature has expired

signature.expired?  # => true or false

Returns:

  • (Boolean)

    true if the signature has expired

Raises:

  • (Error)

    If the ‘expires` parameter is not a valid integer

See Also:



134
135
136
137
138
139
# File 'lib/linzer/signature.rb', line 134

def expired?
  return false if !parameters.key?("expires")
  Time.now.to_i >= Integer(parameters["expires"])
rescue ArgumentError, TypeError
  raise Error.new "Signature has a non-integer `expires` parameter"
end

#field_idsArray<FastIdentifier, FieldId>

Builds FieldId objects for each covered component.

Uses parsed_items when available to create FastIdentifier objects that bypass Starry re-parsing. Falls back to constructing full FieldId objects from the serialized strings.

Returns a fresh array each time because some adapter methods may mutate item parameters during field lookup (e.g., deleting “req”).

Returns:

  • (Array<FastIdentifier, FieldId>)

    FieldId objects for each component



89
90
91
# File 'lib/linzer/signature.rb', line 89

def field_ids
  build_field_ids
end

#older_than?(seconds) ⇒ Boolean

Checks if the signature is older than a given number of seconds.

This is useful for implementing replay attack protection by rejecting signatures that are too old.

Examples:

Check if signature is older than 5 minutes

signature.older_than?(300)  # => true or false

Parameters:

  • seconds (Integer)

    The maximum age in seconds

Returns:

  • (Boolean)

    true if the signature is older than the specified seconds

Raises:

  • (Error)

    If the signature is missing the ‘created` parameter



116
117
118
119
# File 'lib/linzer/signature.rb', line 116

def older_than?(seconds)
  raise Error.new "Signature is missing the `created` parameter" if created.nil?
  (Time.now.to_i - created) > seconds
end

#to_hHash{String => String}

Converts the signature to HTTP header format.

Returns a hash suitable for setting as HTTP headers on a request or response. The hash contains ‘signature` and `signature-input` keys.

Examples:

Attaching to a Net::HTTP request

signature.to_h.each { |name, value| request[name] = value }

Returns:

  • (Hash{String => String})

    Hash with “signature” and “signature-input” keys



150
151
152
153
154
155
156
157
158
159
160
# File 'lib/linzer/signature.rb', line 150

def to_h
  return @headers if @headers

  items = @parsed_items || serialized_components.map { |c| HTTP::StructuredField.parse_item(c) }
  {
    "signature"       => HTTP::StructuredField.serialize({label => value}),
    "signature-input" => HTTP::StructuredField.serialize({
      label => HTTP::StructuredField::InnerList.new(items, parameters)
    })
  }
end