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 = {})
     = .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 
  
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



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/linzer/signature.rb', line 158

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

#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



114
115
116
117
118
119
120
121
122
123
124
# File 'lib/linzer/signature.rb', line 114

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