Module: Familia::VerifiableIdentifier

Extended by:
SecureIdentifier
Defined in:
lib/familia/verifiable_identifier.rb

Overview

Creates and verifies identifiers that contain an embedded HMAC signature, allowing for stateless verification of an identifier's authenticity.

Constant Summary collapse

SECRET_KEY =
Note:

Security Considerations:

  • Secrecy: This key MUST be kept secret and secure, just like a database password or API key. Do not commit it to version control.
  • Consistency: All running instances of your application must use the exact same key, otherwise verification will fail across different servers.
  • Rotation: If this key is ever compromised, it must be rotated. Be aware that rotating the key will invalidate all previously generated verifiable identifiers.

The secret key for HMAC generation, loaded from an environment variable.

This key is the root of trust for verifying identifier authenticity. It must be a long, random, and cryptographically strong string.

Examples:

Generating and Setting the Key

1. Generate a new secure key in your terminal:
   $ openssl rand -hex 32
   > cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d

2. Set it as an environment variable in your production environment:
   export VERIFIABLE_ID_HMAC_SECRET="cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')
RANDOM_HEX_LENGTH =

The length of the random part of the ID in hex characters (256 bits).

64
TAG_HEX_LENGTH =

The length of the HMAC tag in hex characters (64 bits). 64 bits is strong enough to prevent forgery (1 in 18 quintillion chance).

16

Instance Attribute Summary collapse

Class Method Summary collapse

Methods included from SecureIdentifier

generate_id, generate_lite_id, generate_trace_id, shorten_to_trace_id, truncate_hex

Instance Attribute Details

#SECRET_KEYString (readonly)

Returns The secret key.

Returns:

  • (String)

    The secret key.



41
# File 'lib/familia/verifiable_identifier.rb', line 41

SECRET_KEY = ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')

Class Method Details

.generate_verifiable_id(base_or_scope = nil, scope: nil, base: 36) ⇒ String

Generates a verifiable, base-36 encoded identifier.

The final identifier contains a 256-bit random component and a 64-bit authentication tag.

Parameters:

  • base (Integer) (defaults to: 36)

    The base for encoding the output string.

Returns:

  • (String)

    A verifiable, signed identifier.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/familia/verifiable_identifier.rb', line 56

def self.generate_verifiable_id(base_or_scope = nil, scope: nil, base: 36)
  # Handle backward compatibility with positional base argument
  if base_or_scope.is_a?(Integer)
    base = base_or_scope
    # scope remains as passed in keyword argument
  elsif base_or_scope.is_a?(String) || base_or_scope.nil?
    scope = base_or_scope if scope.nil?
    # base remains as passed in keyword argument or default
  end

  # Re-use generate_id from the SecureIdentifier module.
  random_hex = generate_id(16)
  tag_hex = generate_tag(random_hex, scope: scope)

  combined_hex = random_hex + tag_hex

  # Re-use the min_length_for_bits helper from the SecureIdentifier module.
  total_bits = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) * 4
  target_length = Familia::SecureIdentifier.min_length_for_bits(total_bits, base)

  combined_hex.to_i(16).to_s(base).rjust(target_length, '0')
end

.plausible_identifier?(identifier_str, base = 36) ⇒ Boolean

Checks if an identifier is plausible (correct format and length) without performing cryptographic verification.

This can be used as a fast pre-flight check to reject obviously malformed identifiers.

Parameters:

  • identifier_str (String)

    The identifier string to check.

  • base (Integer) (defaults to: 36)

    The base of the input string.

Returns:

  • (Boolean)

    True if the identifier has a valid format, false otherwise.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/familia/verifiable_identifier.rb', line 115

def self.plausible_identifier?(identifier_str, base = 36)
  return false unless identifier_str.is_a?(::String)

  # 1. Check length
  total_bits = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) * 4
  expected_length = Familia::SecureIdentifier.min_length_for_bits(total_bits, base)
  return false unless identifier_str.length == expected_length

  # 2. Check character set
  # The most efficient way to check for invalid characters is to attempt
  # conversion and rescue the error.
  Integer(identifier_str, base)
  true
rescue ArgumentError
  false
end

.verified_identifier?(verifiable_id, base_or_scope = nil, scope: nil, base: 36) ⇒ Boolean

Verifies the authenticity of a given identifier using a timing-safe comparison.

Parameters:

  • verifiable_id (String)

    The identifier string to check.

  • base (Integer) (defaults to: 36)

    The base of the input string.

Returns:

  • (Boolean)

    True if the identifier is authentic, false otherwise.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/familia/verifiable_identifier.rb', line 84

def self.verified_identifier?(verifiable_id, base_or_scope = nil, scope: nil, base: 36)
  # Handle backward compatibility with positional base argument
  if base_or_scope.is_a?(Integer)
    base = base_or_scope
    # scope remains as passed in keyword argument
  elsif base_or_scope.is_a?(String) || base_or_scope.nil?
    scope = base_or_scope if scope.nil?
    # base remains as passed in keyword argument or default
  end

  return false unless plausible_identifier?(verifiable_id, base)

  expected_hex_length = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH)
  combined_hex = verifiable_id.to_i(base).to_s(16).rjust(expected_hex_length, '0')

  random_part = combined_hex[0...RANDOM_HEX_LENGTH]
  tag_part = combined_hex[RANDOM_HEX_LENGTH..]

  expected_tag = generate_tag(random_part, scope: scope)
  OpenSSL.secure_compare(expected_tag, tag_part)
end