Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PR-23132] Add the ability to store certificates in the database #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,31 @@ you the tables and ActiveRecord models, and also an engine with the necessary AP

Now is your turn. Before proceeding, you need to set these ENV variables:
* `PASSKIT_WEB_SERVICE_HOST`
* `PASSKIT_CERTIFICATE_KEY`
* `PASSKIT_PRIVATE_P12_CERTIFICATE`
* `PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE`
* `PASSKIT_APPLE_TEAM_IDENTIFIER`
* `PASSKIT_PASS_TYPE_IDENTIFIER`

We have a [specific guide on how to get all these](docs/passkit_environment_variables.md), please follow it.
You cannot start using this library without these variables set, and we cannot do the work for you.

### Access to certificate data

Use the database or environment variables to access certificate data.
By default, the certificate and its password will be sourced from the database.
If you want to use environment variables, set:
* `PASSKIT_CERTIFICATE_KEY`
* `PASSKIT_CERTIFICATE_PASSWORD`

and update the configuration as follows:
```ruby
Passkit.configure do |config|
config.use_database_for_certificates = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This configuration is not needed if we make the gem accept certificate_source object and call #certificate and #password methods on it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this configuration, ENV variables will be checked each time

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why check ENV variables when certificate_source is passed?

end
```

## Usage

If you followed the installation steps and you have the ENV variables set, we can start looking at what is provided for you.
If you followed the installation steps, we can start looking at what is provided for you.

### Dashboard

Expand Down Expand Up @@ -120,7 +133,7 @@ Again, looking at these examples, is the easiest way to get started.

### Create your own Wallet Pass

You can create your own Wallet Passes by creating a new class that inherits from `Passkit::BasePass` and
You can create your own Wallet Passes by creating a new class that inherits from `Passkit::BasePass` and
defining the methods that you want to override.

You can define colors, fields and texts. You can also define the logo and the background image.
Expand Down Expand Up @@ -152,11 +165,11 @@ Passkit::UrlGenerator.new(Passkit::UserTicket, User.find(1), :tickets)
and then use `.android` or `.ios` to get the URL to serve the Wallet Pass.
Again, check the example mailer included in the gem to see how to use it.

## Debug issues
## Debug issues

* On Mac, you can open the *.pkpass files with "Pass Viewer". Open the `Console.app` to log possible error messages and filter by "Pass Viewer" process.
* Check the logs on http://localhost:3000/passkit/dashboard/logs
* In case of error "The passTypeIdentifier or teamIdentifier provided may not match your certificate,
* In case of error "The passTypeIdentifier or teamIdentifier provided may not match your certificate,
or the certificate trust chain could not be verified." the certificate (p12) might be expired.


Expand Down
7 changes: 3 additions & 4 deletions app/controllers/passkit/api/v1/passes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ def create

if @generator && @payload[:collection_name].present?
files = @generator.public_send(@payload[:collection_name]).collect do |collection_item|
Passkit::Factory.create_pass(@payload[:pass_class], collection_item)
Passkit::Factory.create_pass(@payload[:pass_class], generator: collection_item, site_id: params[:site_id])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have some method that can be redefined in child controller, e.g. pass_attributes instead of using hardcoded site_id in gem? Don’t want gem to know anything about sites.

end
file = Passkit::Generator.compress_passes_files(files)
send_file(file, type: 'application/vnd.apple.pkpasses', disposition: 'attachment')
else
file = Passkit::Factory.create_pass(@payload[:pass_class], @generator)
file = Passkit::Factory.create_pass(@payload[:pass_class], generator: @generator, site_id: params[:site_id])
send_file(file, type: 'application/vnd.apple.pkpass', disposition: 'attachment')
end
end
Expand All @@ -35,11 +35,10 @@ def show
return
end

pass_output_path = Passkit::Generator.new(pass).generate_and_sign

response.headers["last-modified"] = pass.last_update.httpdate
if request.headers["If-Modified-Since"].nil? ||
(pass.last_update.to_i > Time.zone.parse(request.headers["If-Modified-Since"]).to_i)
pass_output_path = Passkit::Generator.new(pass).generate_and_sign
send_file(pass_output_path, type: "application/vnd.apple.pkpass", disposition: "attachment")
else
head :not_modified
Expand Down
8 changes: 5 additions & 3 deletions app/controllers/passkit/api/v1/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module V1
# @see Android: https://walletpasses.io/developer/
class RegistrationsController < ActionController::API
before_action :load_pass, only: %i[create destroy]
before_action :load_device, only: %i[show]
before_action :load_device, only: %i[show destroy]

# @return If the serial number is already registered for this device, returns HTTP status 200.
# @return If registration succeeds, returns HTTP status 201.
Expand Down Expand Up @@ -48,8 +48,10 @@ def show
# @return If the request is not authorized, returns HTTP status 401.
# @return Otherwise, returns the appropriate standard HTTP status.
def destroy
registrations = @pass.registrations.where(passkit_device_id: params[:device_id])
registrations.delete_all
render json: {}, status: :ok unless @device

registrations = @pass.registrations.where(passkit_device_id: @device.id)
registrations.destroy_all
render json: {}, status: :ok
Comment on lines +51 to 55
Copy link
Member

@zhuravel zhuravel Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double render happens here, you need to return after the first render, or better rewrite this to only have a single render.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial changes

good point
will be fixed

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad.

end

Expand Down
4 changes: 4 additions & 0 deletions app/models/passkit/certificate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Passkit
class Certificate < ActiveRecord::Base
end
end
19 changes: 16 additions & 3 deletions lib/passkit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class Configuration
:private_p12_certificate,
:apple_intermediate_certificate,
:apple_team_identifier,
:pass_type_identifier
:pass_type_identifier,
:use_database_for_certificates

DEFAULT_AUTHENTICATION = proc do
authenticate_or_request_with_http_basic("Passkit Dashboard. Login required") do |username, password|
Expand All @@ -43,11 +44,23 @@ def initialize
@available_passes = {"Passkit::ExampleStoreCard" => -> {}}
@web_service_host = ENV["PASSKIT_WEB_SERVICE_HOST"] || (raise "Please set PASSKIT_WEB_SERVICE_HOST")
raise("PASSKIT_WEB_SERVICE_HOST must start with https://") unless @web_service_host.start_with?("https://")
@certificate_key = ENV["PASSKIT_CERTIFICATE_KEY"] || (raise "Please set PASSKIT_CERTIFICATE_KEY")
@private_p12_certificate = ENV["PASSKIT_PRIVATE_P12_CERTIFICATE"] || (raise "Please set PASSKIT_PRIVATE_P12_CERTIFICATE")
@apple_intermediate_certificate = ENV["PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE"] || (raise "Please set PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE")
@apple_team_identifier = ENV["PASSKIT_APPLE_TEAM_IDENTIFIER"] || (raise "Please set PASSKIT_APPLE_TEAM_IDENTIFIER")
@pass_type_identifier = ENV["PASSKIT_PASS_TYPE_IDENTIFIER"] || (raise "Please set PASSKIT_PASS_TYPE_IDENTIFIER")
@use_database_for_certificates = true
validate_certificates
end

def use_database_for_certificates=(value)
@use_database_for_certificates = value
validate_certificates
end

def validate_certificates
unless use_database_for_certificates
@private_p12_certificate = ENV["PASSKIT_PRIVATE_P12_CERTIFICATE"] || (raise "Please set PASSKIT_PRIVATE_P12_CERTIFICATE")
@certificate_key = ENV["PASSKIT_CERTIFICATE_KEY"] || (raise "Please set PASSKIT_CERTIFICATE_KEY")
end
end
end
end
9 changes: 9 additions & 0 deletions lib/passkit/certificate_sources/certificate_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Passkit::CertificateSources::CertificateSource
def certificate
raise NotImplementedError
end

def password
raise NotImplementedError
end
end
22 changes: 22 additions & 0 deletions lib/passkit/certificate_sources/database.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Passkit::CertificateSources::Database

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth of adding db migration as well, but optional.

include Passkit::CertificateSources::CertificateSource

def initialize(site_id)
@site_id = site_id
end

def certificate
certificate_record.certificate
end

def password
certificate_record.secret
end

private

def certificate_record
@certificate_record ||= Passkit::Certificate.find_by!(site_id: @site_id)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely we shouldn't search here by site_id.

end
end

12 changes: 12 additions & 0 deletions lib/passkit/certificate_sources/env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Passkit::CertificateSources::Env
include Passkit::CertificateSources::CertificateSource

def certificate
Rails.root.join(ENV["PASSKIT_PRIVATE_P12_CERTIFICATE"])
end

def password
ENV["PASSKIT_CERTIFICATE_KEY"]
end
end

9 changes: 9 additions & 0 deletions lib/passkit/certificate_sources/factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Passkit::CertificateSources::Factory
def self.find_source(site_id)
if Passkit.configuration.use_database_for_certificates
Passkit::CertificateSources::Database.new(site_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can built this source in Talkable and pass source (instead of site_id) as an argument.

else
Passkit::CertificateSources::Env.new
end
end
end
7 changes: 5 additions & 2 deletions lib/passkit/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ module Passkit
class Factory
class << self
# generator is an optional ActiveRecord object, the application data for the pass
def create_pass(pass_class, generator = nil)
pass = Passkit::Pass.create!(klass: pass_class, generator: generator)
def create_pass(pass_class, generator: nil, site_id: nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO everything site-related is part of Talkable-specific custom logic and should be in Talkable, not in gem.
Changes here should be more universal so that we could open source our changes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is you should not hardcode site_id, not that you should not allow providing extra metadata. That can be some “options” hash that would not be tied to Talkable and would work for much more cases than this one and allow using our gem in other companies potentially.

attributes = { klass: pass_class, generator: generator }
attributes[:site_id] = site_id if site_id

pass = Passkit::Pass.find_or_create_by!(attributes)
Passkit::Generator.new(pass).generate_and_sign
end
end
Expand Down
13 changes: 10 additions & 3 deletions lib/passkit/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def initialize(pass)
end

def generate_and_sign
@pass.instance.prepare_files
check_necessary_files
create_temporary_directory
copy_pass_to_tmp_location
Expand Down Expand Up @@ -98,6 +99,10 @@ def generate_json_pass
pass[:semantics] = @pass.semantics if @pass.semantics
pass[:userInfo] = @pass.user_info if @pass.user_info

unless @pass[:sharing_prohibited]
pass[:sharing] = @pass.sharing if @pass.sharing
end

pass[@pass.pass_type] = {
headerFields: @pass.header_fields,
primaryFields: @pass.primary_fields,
Expand All @@ -123,13 +128,11 @@ def generate_json_manifest
File.write(@manifest_url, manifest.to_json)
end

CERTIFICATE = Rails.root.join(ENV["PASSKIT_PRIVATE_P12_CERTIFICATE"])
INTERMEDIATE_CERTIFICATE = Rails.root.join(ENV["PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE"])
CERTIFICATE_PASSWORD = ENV["PASSKIT_CERTIFICATE_KEY"]

# :nocov:
def sign_manifest
p12_certificate = OpenSSL::PKCS12.new(File.read(CERTIFICATE), CERTIFICATE_PASSWORD)
p12_certificate = OpenSSL::PKCS12.new(File.read(certificate_source.certificate), certificate_source.password)
intermediate_certificate = OpenSSL::X509::Certificate.new(File.read(INTERMEDIATE_CERTIFICATE))

flag = OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY
Expand All @@ -155,5 +158,9 @@ def compress_pass_file
end
zip_path
end

def certificate_source
@certificate_source ||= Passkit::CertificateSources::Factory.find_source(@pass.site_id)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too, pass doesn't have site_id.

end
end
end