Skip to content

Commit

Permalink
add algorithm param to checkouts
Browse files Browse the repository at this point in the history
  • Loading branch information
ezekg committed Jan 29, 2025
1 parent 3087286 commit fd5b8a2
Show file tree
Hide file tree
Showing 14 changed files with 670 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class CheckoutsController < Api::V1::BaseController

typed_query {
param :encrypt, type: :boolean, coerce: true, optional: true
param :algorithm, type: :string, allow_blank: true, optional: true
param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true
param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes {
# FIXME(ezekg) For backwards compatibility. Replace user include with
Expand All @@ -22,6 +23,7 @@ class CheckoutsController < Api::V1::BaseController
}
def show
kwargs = checkout_query.slice(
:algorithm,
:include,
:encrypt,
:ttl,
Expand All @@ -33,6 +35,8 @@ def show
response.headers['Content-Type'] = 'application/octet-stream'

render body: license_file.certificate
rescue LicenseCheckoutService::InvalidAlgorithmError => e
render_bad_request detail: e.message, code: :CHECKOUT_ALGORITHM_INVALID, source: { parameter: :algorithm }
rescue LicenseCheckoutService::InvalidIncludeError => e
render_bad_request detail: e.message, code: :CHECKOUT_INCLUDE_INVALID, source: { parameter: :include }
rescue LicenseCheckoutService::InvalidTTLError => e
Expand All @@ -46,6 +50,7 @@ def show

param :meta, type: :hash, optional: true do
param :encrypt, type: :boolean, optional: true
param :algorithm, type: :string, allow_blank: true, allow_nil: true, optional: true
param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true
param :include, type: :array, allow_blank: true, optional: true, transform: -> key, includes {
includes.push('owner') if includes.delete('user')
Expand All @@ -56,6 +61,7 @@ def show
}
typed_query {
param :encrypt, type: :boolean, coerce: true, optional: true
param :algorithm, type: :string, allow_blank: true, optional: true
param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true
param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes {
includes.push('owner') if includes.delete('user')
Expand All @@ -66,6 +72,7 @@ def show
def create
kwargs = checkout_query.merge(checkout_meta)
.slice(
:algorithm,
:include,
:encrypt,
:ttl,
Expand All @@ -74,6 +81,8 @@ def create
license_file = checkout_license_file(**kwargs)

render jsonapi: license_file
rescue LicenseCheckoutService::InvalidAlgorithmError => e
render_bad_request detail: e.message, code: :CHECKOUT_ALGORITHM_INVALID, source: { parameter: :algorithm }
rescue LicenseCheckoutService::InvalidIncludeError => e
render_bad_request detail: e.message, code: :CHECKOUT_INCLUDE_INVALID, source: { parameter: :include }
rescue LicenseCheckoutService::InvalidTTLError => e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class CheckoutsController < Api::V1::BaseController

typed_query {
param :encrypt, type: :boolean, coerce: true, optional: true
param :algorithm, type: :string, allow_blank: true, optional: true
param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true
param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes {
# FIXME(ezekg) For backwards compatibility. Replace license.user include with
Expand All @@ -22,6 +23,7 @@ class CheckoutsController < Api::V1::BaseController
}
def show
kwargs = checkout_query.slice(
:algorithm,
:include,
:encrypt,
:ttl,
Expand All @@ -33,6 +35,8 @@ def show
response.headers['Content-Type'] = 'application/octet-stream'

render body: machine_file.certificate
rescue MachineCheckoutService::InvalidAlgorithmError => e
render_bad_request detail: e.message, code: :CHECKOUT_ALGORITHM_INVALID, source: { parameter: :algorithm }
rescue MachineCheckoutService::InvalidIncludeError => e
render_bad_request detail: e.message, code: :CHECKOUT_INCLUDE_INVALID, source: { parameter: :include }
rescue MachineCheckoutService::InvalidTTLError => e
Expand All @@ -46,6 +50,7 @@ def show

param :meta, type: :hash, optional: true do
param :encrypt, type: :boolean, optional: true
param :algorithm, type: :string, allow_blank: true, allow_nil: true, optional: true
param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true
param :include, type: :array, allow_blank: true, optional: true, transform: -> key, includes {
includes.push('license.owner') if includes.delete('license.user')
Expand All @@ -56,6 +61,7 @@ def show
}
typed_query {
param :encrypt, type: :boolean, coerce: true, optional: true
param :algorithm, type: :string, allow_blank: true, optional: true
param :ttl, type: :integer, coerce: true, allow_nil: true, optional: true
param :include, type: :array, coerce: true, allow_blank: true, optional: true, transform: -> key, includes {
includes.push('license.owner') if includes.delete('license.user')
Expand All @@ -66,6 +72,7 @@ def show
def create
kwargs = checkout_query.merge(checkout_meta)
.slice(
:algorithm,
:include,
:encrypt,
:ttl,
Expand All @@ -74,6 +81,8 @@ def create
machine_file = checkout_machine_file(**kwargs)

render jsonapi: machine_file
rescue MachineCheckoutService::InvalidAlgorithmError => e
render_bad_request detail: e.message, code: :CHECKOUT_ALGORITHM_INVALID, source: { parameter: :algorithm }
rescue MachineCheckoutService::InvalidIncludeError => e
render_bad_request detail: e.message, code: :CHECKOUT_INCLUDE_INVALID, source: { parameter: :include }
rescue MachineCheckoutService::InvalidTTLError => e
Expand Down
13 changes: 13 additions & 0 deletions app/models/license_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ class LicenseFile
include ActiveModel::Model
include ActiveModel::Attributes

ALGORITHMS = %w[
aes-256-gcm+ed25519
aes-256-gcm+rsa-pss-sha256
aes-256-gcm+rsa-sha256
base64+ed25519
base64+rsa-pss-sha256
base64+rsa-sha256
].freeze

attribute :account_id, :uuid
attribute :environment_id, :uuid
attribute :license_id, :uuid
Expand All @@ -12,6 +21,7 @@ class LicenseFile
attribute :expires_at, :datetime
attribute :ttl, :integer
attribute :includes, :array, default: []
attribute :algorithm, :string

validates :account_id, presence: true
validates :license_id, presence: true
Expand All @@ -29,6 +39,9 @@ class LicenseFile
greater_than_or_equal_to: 1.hour,
allow_nil: true

validates_inclusion_of :algorithm,
in: ALGORITHMS

def persisted? = false
def id = @id ||= SecureRandom.uuid
def product = @product ||= license&.product
Expand Down
13 changes: 13 additions & 0 deletions app/models/machine_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ class MachineFile
include ActiveModel::Model
include ActiveModel::Attributes

ALGORITHMS = %w[
aes-256-gcm+ed25519
aes-256-gcm+rsa-pss-sha256
aes-256-gcm+rsa-sha256
base64+ed25519
base64+rsa-pss-sha256
base64+rsa-sha256
].freeze

attribute :account_id, :uuid
attribute :environment_id, :uuid
attribute :license_id, :uuid
Expand All @@ -13,6 +22,7 @@ class MachineFile
attribute :expires_at, :datetime
attribute :ttl, :integer
attribute :includes, :array, default: []
attribute :algorithm, :string

validates :account_id, presence: true
validates :license_id, presence: true
Expand All @@ -31,6 +41,9 @@ class MachineFile
greater_than_or_equal_to: 1.hour,
allow_nil: true

validates_inclusion_of :algorithm,
in: ALGORITHMS

def persisted? = false
def id = @id ||= SecureRandom.uuid
def product = @product ||= license&.product
Expand Down
1 change: 1 addition & 0 deletions app/serializers/license_file_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class LicenseFileSerializer < BaseSerializer
type 'license-files'

attribute :certificate
attribute :algorithm
attribute :includes
attribute :ttl
attribute :expiry do
Expand Down
1 change: 1 addition & 0 deletions app/serializers/machine_file_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class MachineFileSerializer < BaseSerializer
type 'machine-files'

attribute :certificate
attribute :algorithm
attribute :includes
attribute :ttl
attribute :expiry do
Expand Down
95 changes: 46 additions & 49 deletions app/services/abstract_checkout_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,52 @@ class InvalidAccountError < StandardError; end
class InvalidAlgorithmError < StandardError; end
class InvalidTTLError < StandardError; end

ENCRYPT_ALGORITHM = 'aes-256-gcm'.freeze
ENCODE_ALGORITHM = 'base64'.freeze
ALLOWED_ALGORITHMS = %w[
DEFAULT_ENCRYPTION_ALGORITHM = 'aes-256-gcm'.freeze
DEFAULT_ENCODING_ALGORITHM = 'base64'.freeze
DEFAULT_SIGNING_ALGORITHM = 'ed25519'.freeze

ENCRYPTION_ALGORITHMS = %w[aes-256-gcm].freeze
ENCODING_ALGORITHMS = %w[base64].freeze
SIGNING_ALGORITHMS = %w[
ed25519
rsa-pss-sha256
rsa-sha256
].freeze

def initialize(account:, scheme: nil, encrypt: false, ttl: 1.month, include: [], api_version: nil)
raise InvalidAccountError, 'license must be present' unless
def initialize(account:, encrypt: false, sign: true, algorithm: nil, ttl: 1.month, include: [], api_version: nil)
raise InvalidAccountError, 'account must be present' unless
account.present?

raise InvalidTTLError, 'must be greater than or equal to 3600 (1 hour)' if
ttl.present? && ttl < 1.hour

raise InvalidAlgorithmError, 'algorithm must be present' if
algorithm.present? && algorithm.blank?

@algorithm = algorithm.presence || begin
enc = encrypt ? DEFAULT_ENCRYPTION_ALGORITHM : DEFAULT_ENCODING_ALGORITHM
sig = sign == true || sign.blank? ? DEFAULT_SIGNING_ALGORITHM : sign

"#{enc}+#{sig}"
end

raise InvalidAlgorithmError, 'invalid encoding algorithm' unless
encryption_algorithm.in?(ENCRYPTION_ALGORITHMS) ||
encoding_algorithm.in?(ENCODING_ALGORITHMS)

raise InvalidAlgorithmError, 'invalid signing algorithm' unless
signing_algorithm.in?(SIGNING_ALGORITHMS)

@renderer = Keygen::JSONAPI::Renderer.new(account:, api_version:, context: :checkout)
@account = account
@encrypted = encrypt
@ttl = ttl
@includes = include
@private_key = case scheme
when 'RSA_2048_PKCS1_PSS_SIGN_V2',
'RSA_2048_PKCS1_SIGN_V2',
'RSA_2048_PKCS1_PSS_SIGN',
'RSA_2048_PKCS1_SIGN',
'RSA_2048_PKCS1_ENCRYPT',
'RSA_2048_JWT_RS256'
account.private_key
else
@private_key = case
when ed25519?
account.ed25519_private_key
when rsa?
account.private_key
end

@algorithm = case scheme
when 'RSA_2048_PKCS1_PSS_SIGN_V2',
'RSA_2048_PKCS1_PSS_SIGN'
'rsa-pss-sha256'
when 'RSA_2048_PKCS1_SIGN_V2',
'RSA_2048_PKCS1_SIGN',
'RSA_2048_PKCS1_ENCRYPT',
'RSA_2048_JWT_RS256'
'rsa-sha256'
else
'ed25519'
end
end

def call
Expand All @@ -60,25 +62,23 @@ def call
attr_reader :renderer,
:private_key,
:algorithm,
:encrypted,
:ttl,
:includes,
:account

def encrypted?
!!encrypted
end
def algorithm_parts = @algorithm_parts ||= algorithm.split('+', 2)
def encryption_algorithm = @encryption_algorithm ||= algorithm_parts.first
def signing_algorithm = @signing_algorithm ||= algorithm_parts.second
alias :encoding_algorithm :encryption_algorithm

def encoded?
!encrypted?
end

def ttl?
ttl.present?
end
def encrypted? = encryption_algorithm.in?(ENCRYPTION_ALGORITHMS)
def encoded? = encoding_algorithm.in?(ENCODING_ALGORITHMS)
def ed25519? = signing_algorithm == 'ed25519'
def rsa? = signing_algorithm.in?(%w[rsa-pss-sha256 rsa-sha256])
def ttl? = ttl.present?

def encrypt(value, secret:)
aes = OpenSSL::Cipher.new(ENCRYPT_ALGORITHM)
aes = OpenSSL::Cipher.new(encryption_algorithm)
aes.encrypt

key = OpenSSL::Digest::SHA256.digest(secret)
Expand All @@ -105,24 +105,21 @@ def encode(value, strict: false)
enc.chomp
end

def sign(value, key:, algorithm:, prefix:)
raise InvalidAlgorithmError, 'algorithm is invalid' unless
ALLOWED_ALGORITHMS.include?(algorithm)

def sign(value, prefix:)
data = "#{prefix}/#{value}"

case algorithm
case signing_algorithm
when 'rsa-pss-sha256'
rsa = OpenSSL::PKey::RSA.new(key)
rsa = OpenSSL::PKey::RSA.new(private_key)
sig = rsa.sign_pss(OpenSSL::Digest::SHA256.new, data, salt_length: :max, mgf1_hash: 'SHA256')
when 'rsa-sha256'
rsa = OpenSSL::PKey::RSA.new(key)
rsa = OpenSSL::PKey::RSA.new(private_key)
sig = rsa.sign(OpenSSL::Digest::SHA256.new, data)
when 'ed25519'
ed25519 = Ed25519::SigningKey.new([key].pack('H*'))
ed25519 = Ed25519::SigningKey.new([private_key].pack('H*'))
sig = ed25519.sign(data)
else
raise InvalidAlgorithmError, 'signing scheme is not supported'
raise InvalidAlgorithmError, 'signing algorithm is not supported'
end

encode(sig, strict: true)
Expand Down
27 changes: 20 additions & 7 deletions app/services/license_checkout_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,23 @@ def initialize(license:, environment: nil, include: [], **kwargs)
@license = license
@environment = environment

super(scheme: license.scheme, include:, **kwargs)
# this is used when an algorithm is not explicitly provided
sign = case license.scheme
when 'RSA_2048_PKCS1_PSS_SIGN_V2',
'RSA_2048_PKCS1_PSS_SIGN'
'rsa-pss-sha256'
when 'RSA_2048_PKCS1_SIGN_V2',
'RSA_2048_PKCS1_SIGN',
'RSA_2048_PKCS1_ENCRYPT',
'RSA_2048_JWT_RS256'
'rsa-sha256'
when 'ED25519_SIGN'
'ed25519'
else
true
end

super(sign:, include:, **kwargs)
end

def call
Expand All @@ -45,12 +61,8 @@ def call
else
encode(data, strict: true)
end
sig = sign(enc, key: private_key, algorithm: algorithm, prefix: 'license')
alg = if encrypted?
"#{ENCRYPT_ALGORITHM}+#{algorithm}"
else
"#{ENCODE_ALGORITHM}+#{algorithm}"
end
sig = sign(enc, prefix: 'license')
alg = algorithm

doc = { enc: enc, sig: sig, alg: alg }
enc = encode(doc.to_json)
Expand All @@ -69,6 +81,7 @@ def call
expires_at: expires_at,
ttl: ttl,
includes: incl,
algorithm:,
)
end

Expand Down
Loading

0 comments on commit fd5b8a2

Please sign in to comment.