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 = {}) ⇒ 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 to create Signature instances.



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

def initialize(, value, label, parameters = {})
  @metadata   = .clone.freeze
  @value      = value.clone.freeze
  @parameters = parameters.clone.freeze
  @label      = label.clone.freeze
  freeze
end

Instance Attribute Details

#labelObject (readonly)

Returns the value of attribute label.



54
55
56
# File 'lib/linzer/signature.rb', line 54

def label
  @label
end

#metadataObject (readonly) Also known as: serialized_components

Returns the value of attribute metadata.



41
42
43
# File 'lib/linzer/signature.rb', line 41

def 
  @metadata
end

#parametersObject (readonly)

Returns the value of attribute parameters.



50
51
52
# File 'lib/linzer/signature.rb', line 50

def parameters
  @parameters
end

#valueObject (readonly) Also known as: bytes

Returns the value of attribute value.



45
46
47
# File 'lib/linzer/signature.rb', line 45

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



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/linzer/signature.rb', line 178

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

  signature = parse_structured_field(headers, "signature")
  fail_with_signature_not_found label unless signature.key?(label)

  raw_signature =
    signature[label].value
      .force_encoding(Encoding::ASCII_8BIT)

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

  components = input[label].value.map { |c| Starry.serialize_item(c) }
  parameters = input[label].parameters

  new(components, raw_signature, label, parameters)
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”]`)



73
74
75
# File 'lib/linzer/signature.rb', line 73

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



82
83
84
85
86
87
# File 'lib/linzer/signature.rb', line 82

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:



118
119
120
121
122
123
# File 'lib/linzer/signature.rb', line 118

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

#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



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

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



134
135
136
137
138
139
140
141
142
143
144
# File 'lib/linzer/signature.rb', line 134

def to_h
  {
    "signature"       => Starry.serialize({label => value}),
    "signature-input" => Starry.serialize({
      label => Starry::InnerList.new(
        serialized_components.map { |c| Starry.parse_item(c) },
        parameters
      )
    })
  }
end