Class: Linzer::Signature

Inherits:
Object
  • Object
show all
Defined in:
lib/linzer/signature.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:

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
# File 'lib/linzer/signature.rb', line 234

def build(headers, options = {})
  basic_validate headers
  headers.transform_keys!(&:downcase)
  validate headers

  input = parse_structured_field(headers, "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| Starry.parse_item(c) }
  {
    "signature"       => Starry.serialize({label => value}),
    "signature-input" => Starry.serialize({
      label => Starry::InnerList.new(items, parameters)
    })
  }
end