Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased](https://github.com/jwt/ruby-jwe/tree/HEAD)

**Features:**

- Add `crit` (Critical) header parameter validation per RFC 7516 §4.1.13

## [v1.1.1](https://github.com/jwt/ruby-jwe/tree/v1.1.1) (2025-08-07)

[Full Changelog](https://github.com/jwt/ruby-jwe/compare/v1.1.0...v1.1.1)
Expand Down
24 changes: 24 additions & 0 deletions lib/jwe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ class InvalidData < RuntimeError; end
VALID_ENC = %w[A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM].freeze
VALID_ZIP = ['DEF'].freeze

REGISTERED_HEADERS = %w[
alg enc zip jku jwk kid x5u x5c x5t x5t#S256 typ cty crit
].freeze

class << self
attr_accessor :supported_critical_headers

def encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', **more_headers)
header = generate_header(alg, enc, more_headers)
check_params(header, key)
Expand Down Expand Up @@ -55,6 +61,7 @@ def check_params(header, key)
check_alg(header[:alg] || header['alg'])
check_enc(header[:enc] || header['enc'])
check_zip(header[:zip] || header['zip'])
check_crit(header)
check_key(key)
end

Expand All @@ -74,6 +81,21 @@ def check_key(key)
raise ArgumentError.new('The key must not be nil or blank') if key.nil? || (key.is_a?(String) && key.strip == '')
end

def check_crit(header)
crit = header[:crit] || header['crit']
return if crit.nil?

raise ArgumentError, '"crit" header must be a non-empty array' unless crit.is_a?(Array) && !crit.empty?

crit.each { |param| validate_critical_param(header, param) }
end

def validate_critical_param(header, param)
raise ArgumentError, "\"#{param}\" is a registered header and cannot be in \"crit\"" if REGISTERED_HEADERS.include?(param)
raise ArgumentError, "\"#{param}\" is in \"crit\" but not present in header" unless header.key?(param) || header.key?(param.to_sym)
raise JWE::InvalidData, "Unsupported critical header: \"#{param}\"" unless supported_critical_headers.include?(param)
end

def param_to_class_name(param)
klass = param.gsub(/[-+]/, '_').downcase.sub(/^[a-z\d]*/) { ::Regexp.last_match(0).capitalize }
klass.gsub(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize }
Expand All @@ -98,4 +120,6 @@ def generate_serialization(hdr, cek, content, cipher)
Serialization::Compact.encode(hdr, cek, cipher.iv, content, cipher.tag)
end
end

self.supported_critical_headers = []
end
79 changes: 79 additions & 0 deletions spec/jwe/crit_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require 'spec_helper'

describe JWE do
describe '.check_crit' do
context 'when crit header is not present' do
it 'does not raise an error' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM' }
expect { JWE.check_crit(header) }.not_to raise_error
end
end

context 'when crit header is present' do
context 'with valid critical headers' do
before do
JWE.supported_critical_headers = ['custom-header']
end

after do
JWE.supported_critical_headers = []
end

it 'accepts supported critical headers that exist in the header' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM', crit: ['custom-header'], 'custom-header' => 'value' }
expect { JWE.check_crit(header) }.not_to raise_error
end
end

context 'with invalid critical headers' do
it 'raises an error when crit is not an array' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM', crit: 'not-an-array' }
expect { JWE.check_crit(header) }.to raise_error(ArgumentError, /"crit" header must be a non-empty array/)
end

it 'raises an error when crit is an empty array' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM', crit: [] }
expect { JWE.check_crit(header) }.to raise_error(ArgumentError, /"crit" header must be a non-empty array/)
end

it 'raises an error when crit contains a registered header' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM', crit: ['alg'] }
expect { JWE.check_crit(header) }.to raise_error(ArgumentError, /registered header/)
end

it 'raises an error when crit references a non-existent header' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM', crit: ['missing-header'] }
expect { JWE.check_crit(header) }.to raise_error(ArgumentError, /not present in header/)
end

it 'raises an error when crit contains an unsupported header' do
header = { alg: 'RSA-OAEP', enc: 'A128GCM', crit: ['unsupported'], 'unsupported' => 'value' }
expect { JWE.check_crit(header) }.to raise_error(JWE::InvalidData, /Unsupported critical header/)
end
end
end
end

describe 'encryption/decryption with crit header' do
let(:key) { OpenSSL::PKey::RSA.generate(2048) }
let(:plaintext) { 'Hello, World!' }

context 'with supported critical headers' do
before do
JWE.supported_critical_headers = ['custom-header']
end

after do
JWE.supported_critical_headers = []
end

it 'successfully encrypts and decrypts with crit header' do
encrypted = JWE.encrypt(plaintext, key, crit: ['custom-header'], 'custom-header': 'value')
decrypted = JWE.decrypt(encrypted, key)
expect(decrypted).to eq(plaintext)
end
end
end
end