require 'openssl'
module Spaceship
module Portal
# Represents a certificate from the Apple Developer Portal.
#
# This can either be a code signing identity or a push profile
class Certificate < PortalBase
# @return (String) The ID given from the developer portal. You'll probably not need it.
# @example
# "P577TH3PAA"
attr_accessor :id
# @return (String) The name of the certificate
# @example Company
# "SunApps GmbH"
# @example Push Profile
# "com.krausefx.app"
attr_accessor :name
# @return (String) Status of the certificate
# @example
# "Issued"
attr_accessor :status
# @return (Date) The date and time when the certificate was created
# @example
# 2015-04-01 21:24:00 UTC
attr_accessor :created
# @return (Date) The date and time when the certificate will expire
# @example
# 2016-04-01 21:24:00 UTC
attr_accessor :expires
# @return (String) The owner type that defines if it's a push profile
# or a code signing identity
#
# @example Code Signing Identity
# "team"
# @example Push Certificate
# "bundle"
attr_accessor :owner_type
# @return (String) The name of the owner
#
# @example Code Signing Identity (usually the company name)
# "SunApps Gmbh"
# @example Push Certificate (the name of your App ID)
# "Awesome App"
attr_accessor :owner_name
# @return (String) The ID of the owner, that can be used to
# fetch more information
# @example
# "75B83SPLAA"
attr_accessor :owner_id
# Indicates the type of this certificate
# which is automatically used to determine the class of
# the certificate. Available values listed in CERTIFICATE_TYPE_IDS
# @return (String) The type of the certificate
# @example Production Certificate
# "R58UK2EWSO"
# @example Development Certificate
# "5QPB9NHCEI"
attr_accessor :type_display_id
# @return (Bool) Whether or not the certificate can be downloaded
attr_accessor :can_download
attr_mapping({
'certificateId' => :id,
'name' => :name,
'statusString' => :status,
'dateCreated' => :created,
'expirationDate' => :expires,
'ownerType' => :owner_type,
'ownerName' => :owner_name,
'ownerId' => :owner_id,
'certificateTypeDisplayId' => :type_display_id,
'canDownload' => :can_download
})
#####################################################
# Certs are not associated with apps
#####################################################
# A development code signing certificate used for development environment
class Development < Certificate; end
# A production code signing certificate used for distribution environment
class Production < Certificate; end
# An In House code signing certificate used for enterprise distributions
class InHouse < Certificate; end
# A Mac development code signing certificate used for development environment
class MacDevelopment < Certificate; end
# A Mac production code signing certificate for building .app bundles
class MacAppDistribution < Certificate; end
# A Mac production code signing certificate for building .pkg installers
class MacInstallerDistribution < Certificate; end
# A Mac Developer ID signing certificate for building .app bundles
class DeveloperIDApplication < Certificate; end
# A Mac Developer ID signing certificate for building .pkg installers
class DeveloperIDInstaller < Certificate; end
#####################################################
# Certs that are specific for one app
#####################################################
# Abstract class for push certificates. Check out the subclasses
# DevelopmentPush, ProductionPush, WebsitePush and VoipPush
class PushCertificate < Certificate; end
# A push notification certificate for development environment
class DevelopmentPush < PushCertificate; end
# A push notification certificate for production environment
class ProductionPush < PushCertificate; end
# A push notification certificate for websites
class WebsitePush < PushCertificate; end
# A push notification certificate for the VOIP environment
class VoipPush < PushCertificate; end
# Passbook certificate
class Passbook < Certificate; end
# ApplePay certificate
class ApplePay < Certificate; end
# A Mac push notification certificate for development environment
class MacDevelopmentPush < PushCertificate; end
# A Mac push notification certificate for production environment
class MacProductionPush < PushCertificate; end
IOS_CERTIFICATE_TYPE_IDS = {
"5QPB9NHCEI" => Development,
"R58UK2EWSO" => Production,
"9RQEK7MSXA" => InHouse,
"LA30L5BJEU" => Certificate,
"BKLRAVXMGM" => DevelopmentPush,
"UPV3DW712I" => ProductionPush,
"Y3B2F3TYSI" => Passbook,
"3T2ZP62QW8" => WebsitePush,
"E5D663CMZW" => VoipPush,
"4APLUP237T" => ApplePay
}
OLDER_IOS_CERTIFICATE_TYPES = [
# those are also sent by the browser, but not sure what they represent
"T44PTHVNID",
"DZQUP8189Y",
"FGQUP4785Z",
"S5WE21TULA",
"3BQKVH9I2X", # ProductionPush,
"FUOY7LWJET"
]
MAC_CERTIFICATE_TYPE_IDS = {
"749Y1QAGU7" => MacDevelopment,
"HXZEUKP0FP" => MacAppDistribution,
"2PQI8IDXNH" => MacInstallerDistribution,
"OYVN2GW35E" => DeveloperIDInstaller,
"W0EURJRMC5" => DeveloperIDApplication,
"CDZ7EMXIZ1" => MacProductionPush,
"HQ4KP3I34R" => MacDevelopmentPush,
"DIVN2GW3XT" => DeveloperIDApplication
}
CERTIFICATE_TYPE_IDS = IOS_CERTIFICATE_TYPE_IDS.merge(MAC_CERTIFICATE_TYPE_IDS)
# Class methods
class << self
# Create a new code signing request that can be used to
# generate a new certificate
# @example
# Create a new certificate signing request
# csr, pkey = Spaceship.certificate.create_certificate_signing_request
#
# # Use the signing request to create a new distribution certificate
# Spaceship.certificate.production.create!(csr: csr)
def create_certificate_signing_request
key = OpenSSL::PKey::RSA.new 2048
csr = OpenSSL::X509::Request.new
csr.version = 0
csr.subject = OpenSSL::X509::Name.new([
['CN', 'PEM', OpenSSL::ASN1::UTF8STRING]
])
csr.public_key = key.public_key
csr.sign(key, OpenSSL::Digest::SHA1.new)
return [csr, key]
end
# Create a new object based on a hash.
# This is used to create a new object based on the server response.
def factory(attrs)
# Example:
# => {"name"=>"iOS Distribution: SunApps GmbH",
# "certificateId"=>"XC5PH8DAAA",
# "serialNumber"=>"797E732CCE8B7AAA",
# "status"=>"Issued",
# "statusCode"=>0,
# "expirationDate"=>#<DateTime: 2015-11-25T22:45:50+00:00 ((2457352j,81950s,0n),+0s,2299161j)>,
# "certificatePlatform"=>"ios",
# "certificateType"=>
# {"certificateTypeDisplayId"=>"R58UK2EAAA",
# "name"=>"iOS Distribution",
# "platform"=>"ios",
# "permissionType"=>"distribution",
# "distributionType"=>"store",
# "distributionMethod"=>"app",
# "ownerType"=>"team",
# "daysOverlap"=>364,
# "maxActive"=>2}}
if attrs['certificateType']
# On some accounts this is nested, so we need to flatten it
attrs.merge!(attrs['certificateType'])
attrs.delete('certificateType')
end
# Parse the dates
# rubocop:disable Style/RescueModifier
attrs['expirationDate'] = (Time.parse(attrs['expirationDate']) rescue attrs['expirationDate'])
attrs['dateCreated'] = (Time.parse(attrs['dateCreated']) rescue attrs['dateCreated'])
# rubocop:enable Style/RescueModifier
# Here we go
klass = CERTIFICATE_TYPE_IDS[attrs['certificateTypeDisplayId']]
klass ||= Certificate
klass.client = @client
klass.new(attrs)
end
# @param mac [Bool] Fetches Mac certificates if true. (Ignored if callsed from a subclass)
# @return (Array) Returns all certificates of this account.
# If this is called from a subclass of Certificate, this will
# only include certificates matching the current type.
def all(mac: false)
if self == Certificate # are we the base-class?
type_ids = mac ? MAC_CERTIFICATE_TYPE_IDS : IOS_CERTIFICATE_TYPE_IDS
types = type_ids.keys
types += OLDER_IOS_CERTIFICATE_TYPES unless mac
else
types = [CERTIFICATE_TYPE_IDS.key(self)]
mac = MAC_CERTIFICATE_TYPE_IDS.values.include? self
end
client.certificates(types, mac: mac).map do |cert|
factory(cert)
end
end
# @param mac [Bool] Searches Mac certificates if true
# @return (Certificate) Find a certificate based on the ID of the certificate.
def find(certificate_id, mac: false)
all(mac: mac).find do |c|
c.id == certificate_id
end
end
# Generate a new certificate based on a code certificate signing request
# @param csr (OpenSSL::X509::Request) (required): The certificate signing request to use. Get one using
# `create_certificate_signing_request`
# @param bundle_id (String) (optional): The app identifier this certificate is for.
# This value is only needed if you create a push profile. For normal code signing
# certificates, you must only pass a certificate signing request.
# @example
# # Create a new certificate signing request
# csr, pkey = Spaceship::Certificate.create_certificate_signing_request
#
# # Use the signing request to create a new distribution certificate
# Spaceship::Certificate::Production.create!(csr: csr)
# @return (Certificate): The newly created certificate
def create!(csr: nil, bundle_id: nil)
type = CERTIFICATE_TYPE_IDS.key(self)
# look up the app_id by the bundle_id
if bundle_id
app = Spaceship::App.find(bundle_id)
raise "Could not find app with bundle id '#{bundle_id}'" unless app
app_id = app.app_id
end
# ensure csr is a OpenSSL::X509::Request
csr = OpenSSL::X509::Request.new(csr) if csr.kind_of?(String)
# if this succeeds, we need to save the .cer and the private key in keychain access or wherever they go in linux
response = client.create_certificate!(type, csr.to_pem, app_id)
# munge the response to make it work for the factory
response['certificateTypeDisplayId'] = response['certificateType']['certificateTypeDisplayId']
self.new(response)
end
end
# instance methods
# @return (String) Download the raw data of the certificate without parsing
def download_raw
client.download_certificate(id, type_display_id, mac: mac?)
end
# @return (OpenSSL::X509::Certificate) Downloads and parses the certificate
def download
OpenSSL::X509::Certificate.new(download_raw)
end
# Revoke the certificate. You shouldn't use this method probably.
def revoke!
client.revoke_certificate!(id, type_display_id, mac: mac?)
end
# @return (Bool): Is this certificate a push profile for apps?
def is_push?
self.kind_of? PushCertificate
end
# @return (Bool) Is this a Mac profile?
def mac?
MAC_CERTIFICATE_TYPE_IDS.include? type_display_id
end
end
end
end