Module: PQCrypto::Seal::Format

Defined in:
lib/pq_crypto/seal/format.rb

Defined Under Namespace

Classes: Header, Section

Constant Summary collapse

MAGIC =
"PQCSEAL1".b
VERSION =
1
CONTENT_SUITE_AEGIS256 =
1
WRAP_SUITE_MLKEM768_X25519_AEGIS256 =
1
LOOKUP_HINT =
1
FLAGS =
0
INNER_VERSION =
1
INNER_FLAGS =
0
PADDING_NONE =
0
PADDING_PADME =
1
PADDING_FIXED =
2
PADDING_BUCKETS =
3
PADDING_POLICY_IDS =
{
  PADDING_NONE => true,
  PADDING_PADME => true,
  PADDING_FIXED => true,
  PADDING_BUCKETS => true
}.freeze
PADDING_POLICY_BY_SYMBOL =
{
  nil => PADDING_NONE,
  :none => PADDING_NONE,
  :padme => PADDING_PADME
}.freeze
PAYLOAD_ID_BYTES =
32
NONCE_BYTES =
32
TAG_BYTES =
32
HINT_BYTES =
32
SECTION_ID_BYTES =
32
XWING_PUBLIC_KEY_BYTES =
1216
XWING_CIPHERTEXT_BYTES =
1120
XWING_SHARED_SECRET_BYTES =
32
DEK_BYTES =
32
STANZA_BYTES =
HINT_BYTES + XWING_CIPHERTEXT_BYTES + NONCE_BYTES + DEK_BYTES + TAG_BYTES
DEFAULT_SLOT_SIZE =
2048
MIN_SLOT_SIZE =
2048
MAX_SLOT_SIZE =
8192
SLOT_GRANULARITY =
256
DEFAULT_RECIPIENT_CAPACITY =
4
MAX_RECIPIENT_CAPACITY =
32
MAX_PUBLIC_METADATA_BYTES =
1 * 1024 * 1024
MAX_PRIVATE_METADATA_BYTES =
16 * 1024 * 1024
MAX_HEADER_BYTES =
2 * 1024 * 1024
DEFAULT_MAX_PLAINTEXT_BYTES =
1 * 1024 * 1024 * 1024
DEFAULT_MAX_STAGING_BYTES =
DEFAULT_MAX_PLAINTEXT_BYTES + MAX_PRIVATE_METADATA_BYTES + 64
DEFAULT_MAX_ENVELOPE_BYTES =
MAX_HEADER_BYTES +
(2 + SECTION_ID_BYTES + MAX_RECIPIENT_CAPACITY * MAX_SLOT_SIZE) +
DEFAULT_MAX_STAGING_BYTES +
TAG_BYTES
LIMIT_DEFAULTS =
{
  max_staging_bytes: DEFAULT_MAX_STAGING_BYTES,
  max_plaintext_bytes: DEFAULT_MAX_PLAINTEXT_BYTES,
  max_envelope_bytes: DEFAULT_MAX_ENVELOPE_BYTES
}.freeze
SLOT_FIELDS =
[
  [:hint, HINT_BYTES],
  [:kem_ciphertext, XWING_CIPHERTEXT_BYTES],
  [:wrap_nonce, NONCE_BYTES],
  [:wrapped_dek, DEK_BYTES],
  [:wrap_tag, TAG_BYTES]
].freeze

Class Method Summary collapse

Class Method Details

.build_header(payload_id:, payload_nonce:, recipient_capacity:, slot_size:, padded_inner_length:, public_metadata:, padding_policy_id:) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/pq_crypto/seal/format.rb', line 133

def build_header(payload_id:, payload_nonce:, recipient_capacity:, slot_size:,
                 padded_inner_length:, public_metadata:, padding_policy_id:)
   = String().b
  raise InvalidConfigurationError, "public metadata is too large" if .bytesize > MAX_PUBLIC_METADATA_BYTES
  raise InvalidConfigurationError, "invalid payload_id" unless payload_id.bytesize == PAYLOAD_ID_BYTES
  raise InvalidConfigurationError, "invalid payload nonce" unless payload_nonce.bytesize == NONCE_BYTES
  validate_padding_policy_id!(padding_policy_id)

  body = +"".b
  body << Binary.u16(CONTENT_SUITE_AEGIS256)
  body << Binary.u8(LOOKUP_HINT)
  body << Binary.u16(FLAGS)
  body << Binary.u8(Integer(padding_policy_id))
  body << payload_id
  body << payload_nonce
  body << Binary.u16(recipient_capacity)
  body << Binary.u16(slot_size)
  body << Binary.u64(padded_inner_length)
  body << Binary.u32(.bytesize)
  body << 
  header = MAGIC + Binary.u8(VERSION) + Binary.u32(MAGIC.bytesize + 1 + 4 + body.bytesize) + body
  raise InvalidConfigurationError, "header is too large" if header.bytesize > MAX_HEADER_BYTES
  header
end

.check_resource_limits!(padded_inner_length:, envelope_bytes: nil, max_staging_bytes: DEFAULT_MAX_STAGING_BYTES, max_envelope_bytes: DEFAULT_MAX_ENVELOPE_BYTES) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/pq_crypto/seal/format.rb', line 253

def check_resource_limits!(padded_inner_length:, envelope_bytes: nil,
                           max_staging_bytes: DEFAULT_MAX_STAGING_BYTES,
                           max_envelope_bytes: DEFAULT_MAX_ENVELOPE_BYTES)
  if padded_inner_length > Integer(max_staging_bytes)
    raise ResourceLimitError,
          "padded_inner_length #{padded_inner_length} exceeds max_staging_bytes #{max_staging_bytes}"
  end
  if envelope_bytes && envelope_bytes > Integer(max_envelope_bytes)
    raise ResourceLimitError,
          "envelope #{envelope_bytes} exceeds max_envelope_bytes #{max_envelope_bytes}"
  end
  true
end

.inner_prefix(content_length, private_metadata_length) ⇒ Object



233
234
235
236
# File 'lib/pq_crypto/seal/format.rb', line 233

def inner_prefix(content_length, )
  Binary.u8(INNER_VERSION) + Binary.u8(INNER_FLAGS) +
    Binary.u64(content_length) + Binary.u32()
end

.padding_policy_id_for(policy) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/pq_crypto/seal/format.rb', line 119

def padding_policy_id_for(policy)
  return PADDING_POLICY_BY_SYMBOL.fetch(policy) if PADDING_POLICY_BY_SYMBOL.key?(policy)
  case policy
  when :preserve
    raise InvalidConfigurationError, "padding: :preserve is only valid for rotate_dek"
  when Hash
    return PADDING_FIXED if policy.key?(:to)
    return PADDING_BUCKETS if policy.key?(:buckets)
    raise InvalidConfigurationError, "padding hash must contain :to or :buckets"
  else
    raise InvalidConfigurationError, "unsupported padding policy: #{policy.inspect}"
  end
end

.parse_header(bytes) ⇒ Object

Raises:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/pq_crypto/seal/format.rb', line 158

def parse_header(bytes)
  bytes = String(bytes).b
  offset = 0
  magic, offset = Binary.read_bytes(bytes, offset, MAGIC.bytesize)
  raise FormatError, "invalid envelope magic" unless magic == MAGIC
  version, offset = Binary.read_u8(bytes, offset)
  raise FormatError, "unsupported format version #{version}" unless version == VERSION
  header_length, offset = Binary.read_u32(bytes, offset)
  raise FormatError, "invalid header length" if header_length < offset || header_length > MAX_HEADER_BYTES
  Binary.ensure_available!(bytes, 0, header_length)
  raw = bytes.byteslice(0, header_length).b

  content_suite, offset = Binary.read_u16(raw, offset)
  raise UnsupportedSuiteError, "unsupported content suite #{content_suite}" unless content_suite == CONTENT_SUITE_AEGIS256
  lookup_mode, offset = Binary.read_u8(raw, offset)
  raise UnsupportedSuiteError, "unsupported lookup mode #{lookup_mode}" unless lookup_mode == LOOKUP_HINT
  flags, offset = Binary.read_u16(raw, offset)
  raise FormatError, "unknown header flags" unless flags == FLAGS
  padding_policy_id, offset = Binary.read_u8(raw, offset)
  validate_padding_policy_id!(padding_policy_id)
  payload_id, offset = Binary.read_bytes(raw, offset, PAYLOAD_ID_BYTES)
  payload_nonce, offset = Binary.read_bytes(raw, offset, NONCE_BYTES)
  capacity, offset = Binary.read_u16(raw, offset)
  validate_capacity_from_wire!(capacity)
  slot_size, offset = Binary.read_u16(raw, offset)
  validate_slot_size_from_wire!(slot_size)
  padded_inner_length, offset = Binary.read_u64(raw, offset)
  raise FormatError, "padded inner frame is too short" if padded_inner_length < 14
  public_length, offset = Binary.read_u32(raw, offset)
  raise FormatError, "public metadata is too large" if public_length > MAX_PUBLIC_METADATA_BYTES
  , offset = Binary.read_bytes(raw, offset, public_length)
  raise FormatError, "non-canonical header length" unless offset == raw.bytesize

  Header.new(
    raw: raw, content_suite_id: content_suite, lookup_mode: lookup_mode,
    flags: flags, padding_policy_id: padding_policy_id,
    payload_id: payload_id, payload_nonce: payload_nonce,
    recipient_capacity: capacity, slot_size: slot_size,
    padded_inner_length: padded_inner_length, public_metadata: 
  )
end

.parse_section(bytes, offset, header) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/pq_crypto/seal/format.rb', line 219

def parse_section(bytes, offset, header)
  length = section_length(header)
  raw, = Binary.read_bytes(bytes, offset, length)
  wrap_suite, slot_offset = Binary.read_u16(raw, 0)
  section_id, slot_offset = Binary.read_bytes(raw, slot_offset, SECTION_ID_BYTES)
  unless wrap_suite == WRAP_SUITE_MLKEM768_X25519_AEGIS256
    raise UnsupportedSuiteError, "unsupported wrap suite #{wrap_suite}"
  end
  Section.new(
    wrap_suite_id: wrap_suite, section_id: section_id, raw: raw,
    slots_offset: slot_offset, slots_length: raw.bytesize - slot_offset
  )
end

.parse_verified_inner(bytes) ⇒ Object

Raises:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/pq_crypto/seal/format.rb', line 238

def parse_verified_inner(bytes)
  bytes = String(bytes).b
  offset = 0
  version, offset = Binary.read_u8(bytes, offset)
  raise FormatError, "unsupported inner version" unless version == INNER_VERSION
  flags, offset = Binary.read_u8(bytes, offset)
  raise FormatError, "unknown inner flags" unless flags == INNER_FLAGS
  content_length, offset = Binary.read_u64(bytes, offset)
  , offset = Binary.read_u32(bytes, offset)
  raise FormatError, "private metadata is too large" if  > MAX_PRIVATE_METADATA_BYTES
  , offset = Binary.read_bytes(bytes, offset, )
  content, offset = Binary.read_bytes(bytes, offset, content_length)
  [content, , bytes.bytesize - offset]
end

.pre_auth_limits(limits) ⇒ Object



267
268
269
270
271
272
# File 'lib/pq_crypto/seal/format.rb', line 267

def pre_auth_limits(limits)
  {
    max_staging_bytes: limits.fetch(:max_staging_bytes, DEFAULT_MAX_STAGING_BYTES),
    max_envelope_bytes: limits.fetch(:max_envelope_bytes, DEFAULT_MAX_ENVELOPE_BYTES)
  }
end

.section_length(header) ⇒ Object



200
201
202
# File 'lib/pq_crypto/seal/format.rb', line 200

def section_length(header)
  section_length_for(header.recipient_capacity, header.slot_size)
end

.section_length_for(capacity, slot_size) ⇒ Object



204
205
206
# File 'lib/pq_crypto/seal/format.rb', line 204

def section_length_for(capacity, slot_size)
  2 + SECTION_ID_BYTES + Integer(capacity) * Integer(slot_size)
end

.split_slot(slot, slot_size) ⇒ Object



208
209
210
211
212
213
214
215
216
217
# File 'lib/pq_crypto/seal/format.rb', line 208

def split_slot(slot, slot_size)
  fields = {}
  offset = 0
  SLOT_FIELDS.each do |name, length|
    fields[name] = slot.byteslice(offset, length)
    offset += length
  end
  fields[:padding] = slot.byteslice(offset, slot_size - offset)
  fields
end

.validate_capacity!(capacity, recipient_count = nil, error: InvalidConfigurationError) ⇒ Object



94
95
96
97
98
99
100
101
102
103
# File 'lib/pq_crypto/seal/format.rb', line 94

def validate_capacity!(capacity, recipient_count = nil, error: InvalidConfigurationError)
  n = Integer(capacity)
  unless n.between?(1, MAX_RECIPIENT_CAPACITY)
    raise error, "recipient_capacity must be 1..#{MAX_RECIPIENT_CAPACITY}"
  end
  if recipient_count && recipient_count > n
    raise RecipientCapacityExceeded, "#{recipient_count} recipients do not fit capacity #{n}"
  end
  n
end

.validate_capacity_from_wire!(capacity) ⇒ Object



109
110
111
# File 'lib/pq_crypto/seal/format.rb', line 109

def validate_capacity_from_wire!(capacity)
  validate_capacity!(capacity, error: FormatError)
end

.validate_padding_policy_id!(policy_id) ⇒ Object

Raises:



113
114
115
116
117
# File 'lib/pq_crypto/seal/format.rb', line 113

def validate_padding_policy_id!(policy_id)
  n = Integer(policy_id)
  raise FormatError, "unsupported padding policy id #{n}" unless PADDING_POLICY_IDS.key?(n)
  n
end

.validate_slot_size!(slot_size, error: InvalidConfigurationError) ⇒ Object

Raises:

  • (error)


85
86
87
88
89
90
91
92
# File 'lib/pq_crypto/seal/format.rb', line 85

def validate_slot_size!(slot_size, error: InvalidConfigurationError)
  n = Integer(slot_size)
  unless n.between?(MIN_SLOT_SIZE, MAX_SLOT_SIZE) && (n % SLOT_GRANULARITY).zero?
    raise error, "slot_size must be #{MIN_SLOT_SIZE}..#{MAX_SLOT_SIZE} and divisible by #{SLOT_GRANULARITY}"
  end
  raise error, "slot_size is too small for current wrap suite" if n < STANZA_BYTES
  n
end

.validate_slot_size_from_wire!(slot_size) ⇒ Object



105
106
107
# File 'lib/pq_crypto/seal/format.rb', line 105

def validate_slot_size_from_wire!(slot_size)
  validate_slot_size!(slot_size, error: FormatError)
end