├── example-server ├── config.ru ├── Gemfile └── application.rb ├── Gemfile ├── lib ├── ios-cert-enrollment │ ├── version.rb │ ├── certificate.rb │ ├── device.rb │ ├── ssl.rb │ ├── configuration.rb │ ├── sign.rb │ └── profile.rb └── ios-cert-enrollment.rb ├── .gitignore ├── test ├── test_ios-cert-enrollment.rb └── helper.rb ├── README.rdoc ├── LICENSE └── ios-cert-enrollment.gemspec /example-server/config.ru: -------------------------------------------------------------------------------- 1 | require './application' 2 | run Sinatra::Application -------------------------------------------------------------------------------- /example-server/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | gem 'sinatra' 3 | gem 'ios-cert-enrollment' -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gem "plist" 3 | gem "uuidtools" 4 | gem "bundler" 5 | 6 | -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/version.rb: -------------------------------------------------------------------------------- 1 | module IOSCertEnrollment 2 | VERSION = '0.0.8'.freeze unless defined?(::IOSCertEnrollment::VERSION) 3 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .DS_Store 4 | .bundle 5 | .rvmrc 6 | .yardoc 7 | .rake_tasks~ 8 | Gemfile.lock 9 | coverage/* 10 | doc/* 11 | log/* 12 | pkg/* 13 | 14 | test/internal 15 | example/ 16 | -------------------------------------------------------------------------------- /test/test_ios-cert-enrollment.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestIosCertEnrollment < Test::Unit::TestCase 4 | 5 | # should "probably rename this file and start testing for real" do 6 | # flunk "hey buddy, you should probably rename this file and start testing for real" 7 | # end 8 | end 9 | -------------------------------------------------------------------------------- /lib/ios-cert-enrollment.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../ios-cert-enrollment/configuration', __FILE__) 2 | require File.expand_path('../ios-cert-enrollment/sign', __FILE__) 3 | require File.expand_path('../ios-cert-enrollment/profile', __FILE__) 4 | require File.expand_path('../ios-cert-enrollment/device', __FILE__) 5 | 6 | module IOSCertEnrollment 7 | extend Configuration 8 | end -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/certificate.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../configuration', __FILE__) 2 | require "openssl" 3 | 4 | module IOSCertEnrollment 5 | class Certificate 6 | 7 | attr_accessor :certificate, :mime_type 8 | 9 | def initialize(certificate,mime_type) 10 | self.certificate = certificate 11 | self.mime_type = mime_type 12 | 13 | end 14 | 15 | end 16 | end -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/device.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../configuration', __FILE__) 2 | require File.expand_path('../certificate', __FILE__) 3 | require File.expand_path('../ssl', __FILE__) 4 | 5 | require "plist" 6 | 7 | module IOSCertEnrollment 8 | module Device 9 | class << self 10 | def parse(p7sign) 11 | return Plist::parse_xml(p7sign.data) 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = ios-cert-enrollment 2 | 3 | Sign and distribute iOS Enrollment certificates using your server's SSL Certificate 4 | 5 | == Usage 6 | 7 | See the example-server directory for usage including available parameters. It can't be run out of the box and requires a SSL certificate. 8 | 9 | TODO: Write SSL generation script 10 | TODO: Write implementation instructions 11 | TODO: Write tests 12 | 13 | == Copyright 14 | 15 | Copyright (c) 2012 Nolan B.. See LICENSE for further details. 16 | 17 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'test/unit' 11 | require 'shoulda' 12 | 13 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 14 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 15 | require 'ios-cert-enrollment' 16 | 17 | class Test::Unit::TestCase 18 | end 19 | -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/ssl.rb: -------------------------------------------------------------------------------- 1 | module IOSCertEnrollment 2 | module SSL 3 | @@key, @@certificate = nil 4 | class << self 5 | def key 6 | return @@key if @@key 7 | return @@key = OpenSSL::PKey::RSA.new(File.read(IOSCertEnrollment.ssl_key_path)) 8 | end 9 | 10 | def certificate 11 | return @@certificate if @@certificate 12 | return @@certificate = OpenSSL::X509::Certificate.new(File.read(IOSCertEnrollment.ssl_certificate_path)) 13 | end 14 | end 15 | 16 | end 17 | end -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/configuration.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../version', __FILE__) 2 | 3 | module IOSCertEnrollment 4 | # Defines constants and methods related to configuration 5 | module Configuration 6 | VALID_OPTIONS_KEYS = [ 7 | :ssl_certificate_path, 8 | :ssl_key_path, 9 | :base_url, 10 | :identifier, 11 | :display_name, 12 | :organization 13 | ].freeze 14 | 15 | attr_accessor *VALID_OPTIONS_KEYS 16 | 17 | # Convenience method to allow configuration options to be set in a block 18 | def configure 19 | yield self 20 | end 21 | 22 | # Create a hash of options and their values 23 | def options 24 | VALID_OPTIONS_KEYS.inject({}) do |option, key| 25 | option.merge!(key => send(key)) 26 | end 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nolan Brown 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /ios-cert-enrollment.gemspec: -------------------------------------------------------------------------------- 1 | # Ensure we require the local version and not one we might have installed already 2 | require File.join([File.dirname(__FILE__),'lib','ios-cert-enrollment.rb']) 3 | spec = Gem::Specification.new do |s| 4 | s.name = 'ios-cert-enrollment' 5 | s.version = "0.0.8" 6 | s.author = 'Nolan Brown' 7 | s.email = 'nolanbrown@gmail.com' 8 | s.homepage = 'https://github.com/nolanbrown/ios-cert-enrollment' 9 | s.platform = Gem::Platform::RUBY 10 | s.summary = 'SCEP server for iOS Configuration Profiles' 11 | s.description = 'Easy tools to implement a SCEP server for iOS Configuration Profiles' 12 | # Add your other files here if you make them 13 | s.files = %w( 14 | lib/ios-cert-enrollment.rb 15 | lib/ios-cert-enrollment/certificate.rb 16 | lib/ios-cert-enrollment/configuration.rb 17 | lib/ios-cert-enrollment/device.rb 18 | lib/ios-cert-enrollment/profile.rb 19 | lib/ios-cert-enrollment/sign.rb 20 | lib/ios-cert-enrollment/ssl.rb 21 | lib/ios-cert-enrollment/version.rb 22 | ) 23 | s.require_paths << 'lib' 24 | s.rdoc_options << '--title' << 'iOS Configuration Profiles' << '--main' #<< 'README.rdoc' << '-ri' 25 | s.bindir = 'bin' 26 | s.add_development_dependency('rake') 27 | s.add_development_dependency('rdoc') 28 | s.add_runtime_dependency('uuidtools') 29 | s.add_runtime_dependency('plist') 30 | end 31 | -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/sign.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../configuration', __FILE__) 2 | require File.expand_path('../certificate', __FILE__) 3 | require File.expand_path('../ssl', __FILE__) 4 | 5 | require "openssl" 6 | 7 | module IOSCertEnrollment 8 | module Sign 9 | 10 | class << self 11 | def registration_authority 12 | return Certificate.new(SSL.certificate.to_der, "application/x-x509-ca-cert") 13 | end 14 | 15 | def certificate_authority_caps 16 | return "POSTPKIOperation\nSHA-1\nDES3\n" 17 | end 18 | 19 | def sign_PKI(data) 20 | 21 | p7sign = OpenSSL::PKCS7.new(data) 22 | store = OpenSSL::X509::Store.new 23 | p7sign.verify(nil, store, nil, OpenSSL::PKCS7::NOVERIFY) 24 | signers = p7sign.signers 25 | p7enc = OpenSSL::PKCS7.new(p7sign.data) 26 | 27 | # Certificate Signing Request 28 | csr = p7enc.decrypt(SSL.key, SSL.certificate) 29 | 30 | # Signed Certificate 31 | cert = self.sign_certificate(csr) 32 | 33 | degenerate_pkcs7 = OpenSSL::PKCS7.new() 34 | degenerate_pkcs7.type="signed" 35 | degenerate_pkcs7.certificates=[cert] 36 | enc_cert = OpenSSL::PKCS7.encrypt(p7sign.certificates, degenerate_pkcs7.to_der, 37 | OpenSSL::Cipher::Cipher::new("des-ede3-cbc"), OpenSSL::PKCS7::BINARY) 38 | reply = OpenSSL::PKCS7.sign(SSL.certificate, SSL.key, enc_cert.to_der, [], OpenSSL::PKCS7::BINARY) 39 | 40 | return Certificate.new(reply.to_der, "application/x-pki-message") 41 | end 42 | 43 | def verify_response(raw_postback_data) 44 | p7sign = OpenSSL::PKCS7.new(raw_postback_data) 45 | store = OpenSSL::X509::Store.new 46 | p7sign.verify(nil, store, nil, OpenSSL::PKCS7::NOVERIFY) 47 | return p7sign 48 | end 49 | def verify_signer(p7sign) 50 | signers = p7sign.signers 51 | 52 | return (signers[0].issuer.to_s == SSL.certificate.subject.to_s) 53 | end 54 | 55 | 56 | end 57 | private 58 | def self.sign_certificate(signing_request) 59 | request = OpenSSL::X509::Request.new(signing_request) 60 | 61 | # New Certificate 62 | cert = OpenSSL::X509::Certificate.new 63 | cert.version = 2 64 | 65 | unix_serial = Time.now.to_f.round(2).to_s.gsub(".","") 66 | (unix_serial.length - 12).abs.times { 67 | unix_serial << "0" 68 | } 69 | cert.serial = unix_serial.to_i 70 | cert.subject = request.subject 71 | cert.issuer = SSL.certificate.subject 72 | cert.public_key = request.public_key 73 | cert.not_before = Time.now 74 | cert.not_after = Time.now+(86400*1) 75 | 76 | # Prepare to sign 77 | ef = OpenSSL::X509::ExtensionFactory.new 78 | ef.subject_certificate = cert 79 | ef.issuer_certificate = SSL.certificate 80 | cert.add_extension(ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true)) 81 | cert.sign(SSL.key, OpenSSL::Digest::SHA1.new) 82 | 83 | return cert 84 | end 85 | 86 | end 87 | end -------------------------------------------------------------------------------- /example-server/application.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | require 'ios-cert-enrollment' 4 | 5 | require 'sinatra/base' 6 | require 'webrick' 7 | require 'webrick/https' 8 | require 'openssl' 9 | 10 | IOSCertEnrollment.configure do |config| 11 | config.ssl_certificate_path = "" 12 | config.ssl_key_path = "" 13 | config.base_url = "" 14 | config.identifier = "com.nolanbrown" 15 | config.display_name = "iOS Enrollment Server" 16 | config.organization = "Nolan Brown" 17 | end 18 | 19 | webrick_options = { 20 | :Port => 8443, 21 | :Logger => WEBrick::Log::new($stderr, WEBrick::Log::DEBUG), 22 | :DocumentRoot => "/ruby/htdocs", 23 | #:DoNotReverseLookup => false, 24 | :SSLEnable => true, 25 | :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, 26 | :SSLCertificate => IOSCertEnrollment::SSL.certificate, 27 | :SSLPrivateKey => IOSCertEnrollment::SSL.key, 28 | :SSLCertName => [ [ "CN",WEBrick::Utils::getservername ] ] 29 | } 30 | 31 | class MyServer < Sinatra::Base 32 | 33 | get '/' do 34 | 'Enroll' 35 | end 36 | 37 | get '/enroll' do 38 | signed_certificate = IOSCertEnrollment::Profile.new("/profile").service().sign() 39 | 40 | ## Send 41 | content_type signed_certificate.mime_type 42 | signed_certificate.certificate 43 | 44 | end 45 | 46 | post '/profile' do 47 | p7sign = IOSCertEnrollment::Sign.verify_response(request.body.read) 48 | if IOSCertEnrollment::Sign.verify_signer(p7sign) 49 | 50 | profile = IOSCertEnrollment::Profile.new() 51 | profile.icon = File.open(File.expand_path('', __FILE__)) 52 | profile.display_name = "iOS Enrollment Server" 53 | profile.description = "Easy access to web" 54 | profile.label = "iOS Enrollment" 55 | profile.url = "" 56 | encrypted_profile = profile.webclip().encrypt(p7sign.certificates) 57 | signed_profile = profile.configuration(encrypted_profile.certificate).sign() 58 | 59 | else 60 | # Get returned device attributes 61 | device_attributes = IOSCertEnrollment::Device.parse(p7sign) 62 | 63 | # "UDID", 64 | # "VERSION", 65 | # "PRODUCT", 66 | # "DEVICE_NAME", 67 | # "MAC_ADDRESS_EN0", 68 | # "IMEI", 69 | # "ICCID" 70 | 71 | ## Validation 72 | profile = IOSCertEnrollment::Profile.new("/scep") 73 | signed_profile = profile.encrypted_service().sign() 74 | 75 | end 76 | ## Send 77 | content_type signed_profile.mime_type 78 | signed_profile.certificate 79 | 80 | end 81 | 82 | get '/scep' do 83 | case params['operation'] 84 | when "GetCACert" 85 | registration_authority = IOSCertEnrollment::Sign.registration_authority 86 | content_type registration_authority.mime_type 87 | registration_authority.certificate 88 | 89 | when "GetCACaps" 90 | content_type "text/plain" 91 | IOSCertEnrollment::Sign.certificate_authority_caps 92 | else 93 | "Invalid Action" 94 | end 95 | end 96 | 97 | post '/scep' do 98 | if params['operation'] == "PKIOperation" 99 | signed_pki = IOSCertEnrollment::Sign.sign_PKI(request.body.read) 100 | 101 | content_type signed_pki.mime_type 102 | signed_pki.certificate 103 | 104 | else 105 | "Invalid Action" 106 | end 107 | end 108 | end 109 | 110 | Rack::Handler::WEBrick.run MyServer, webrick_options 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /lib/ios-cert-enrollment/profile.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../configuration', __FILE__) 2 | require "rubygems" 3 | require "uuidtools" 4 | require "plist" 5 | module IOSCertEnrollment 6 | class Profile 7 | attr_accessor :url, :identifier, :display_name, :description, :icon, :payload, :organization, :expiration, :label 8 | def initialize(url="") 9 | self.url = IOSCertEnrollment.base_url + url 10 | self.identifier = IOSCertEnrollment.identifier 11 | self.display_name = IOSCertEnrollment.display_name 12 | self.organization = IOSCertEnrollment.organization 13 | self.description = "" 14 | self.expiration = nil 15 | self.icon = nil 16 | end 17 | 18 | def service 19 | payload = general_payload() 20 | payload['PayloadType'] = "Profile Service" # do not modify 21 | payload['PayloadIdentifier'] = self.identifier+".mobileconfig.profile-service" 22 | 23 | # strings that show up in UI, customizable 24 | payload['PayloadDisplayName'] = self.display_name 25 | payload['PayloadDescription'] = self.description 26 | 27 | payload_content = Hash.new 28 | payload_content['URL'] = self.url 29 | payload_content['DeviceAttributes'] = [ 30 | "UDID", 31 | "VERSION", 32 | "PRODUCT", # ie. iPhone1,1 or iPod2,1 33 | "DEVICE_NAME", # given device name "My iPhone" 34 | "MAC_ADDRESS_EN0", 35 | "IMEI", 36 | "ICCID" 37 | ]; 38 | 39 | payload['PayloadContent'] = payload_content 40 | self.payload = Plist::Emit.dump(payload) 41 | return self 42 | end 43 | 44 | def encrypted_service 45 | ## ASA encryption_cert_payload 46 | payload = general_payload() 47 | 48 | payload['PayloadIdentifier'] = self.identifier+".encrypted-profile-service" 49 | payload['PayloadType'] = "Configuration" # do not modify 50 | 51 | # strings that show up in UI, customisable 52 | payload['PayloadDisplayName'] = self.display_name 53 | payload['PayloadDescription'] = self.description 54 | 55 | payload['PayloadContent'] = [encryption_cert_request("Profile Service")]; 56 | self.payload = Plist::Emit.dump(payload) 57 | return self 58 | end 59 | 60 | 61 | def webclip 62 | 63 | content_payload = general_payload() 64 | content_payload['PayloadIdentifier'] = self.identifier+".webclip.intranet" 65 | content_payload['PayloadType'] = "com.apple.webClip.managed" # do not modify 66 | 67 | # strings that show up in UI, customisable 68 | content_payload['PayloadDisplayName'] = self.display_name 69 | content_payload['PayloadDescription'] = self.description 70 | 71 | # allow user to remove webclip 72 | content_payload['IsRemovable'] = true 73 | content_payload['Precomposed'] = true 74 | 75 | content_payload['Icon'] = self.icon if self.icon 76 | # the link 77 | content_payload['Label'] = self.label 78 | content_payload['URL'] = self.url 79 | 80 | self.payload = Plist::Emit.dump([content_payload]) 81 | return self 82 | end 83 | 84 | 85 | 86 | def configuration(encrypted_content) 87 | payload = general_payload() 88 | payload['PayloadIdentifier'] = self.identifier+".intranet" 89 | payload['PayloadType'] = "Configuration" # do not modify 90 | 91 | # strings that show up in UI, customisable 92 | payload['PayloadDisplayName'] = self.display_name 93 | payload['PayloadDescription'] = self.description 94 | payload['PayloadExpirationDate'] = self.expiration || Date.today + (360 * 10) # expire in 10 years 95 | 96 | payload['EncryptedPayloadContent'] = StringIO.new(encrypted_content) 97 | self.payload = Plist::Emit.dump(payload) 98 | return self 99 | end 100 | 101 | 102 | def sign 103 | signed_profile = OpenSSL::PKCS7.sign(SSL.certificate, SSL.key, self.payload, [], OpenSSL::PKCS7::BINARY) 104 | return Certificate.new(signed_profile.to_der, "application/x-apple-aspen-config") 105 | 106 | end 107 | 108 | def encrypt(certificates) 109 | encrypted_profile = OpenSSL::PKCS7.encrypt(certificates, self.payload, OpenSSL::Cipher::Cipher::new("des-ede3-cbc"), OpenSSL::PKCS7::BINARY) 110 | return Certificate.new(encrypted_profile.to_der, "application/x-apple-aspen-config") 111 | 112 | end 113 | 114 | 115 | private 116 | def encryption_cert_request(purpose) 117 | ## AKA scep_cert_payload 118 | payload = general_payload() 119 | 120 | 121 | payload['PayloadIdentifier'] = self.identifier+".encryption-cert-request" 122 | payload['PayloadType'] = "com.apple.security.scep" # do not modify 123 | 124 | payload['PayloadDisplayName'] = purpose 125 | payload['PayloadDescription'] = "Provides device encryption identity" 126 | 127 | payload_content = Hash.new 128 | payload_content['URL'] = self.url 129 | payload_content['Subject'] = [ [ [ "O", self.organization ] ], 130 | [ [ "CN", purpose + " (" + UUIDTools::UUID.random_create().to_s + ")" ] ] ]; 131 | 132 | payload_content['Keysize'] = 1024 133 | payload_content['Key Type'] = "RSA" 134 | payload_content['Key Usage'] = 5 # digital signature (1) | key encipherment (4) 135 | payload_content['GetCACaps'] = ["POSTPKIOperation","Renewal","SHA-1"] 136 | 137 | payload['PayloadContent'] = payload_content; 138 | payload 139 | end 140 | 141 | def general_payload() 142 | payload = Hash.new 143 | payload['PayloadVersion'] = 1 # do not modify 144 | payload['PayloadUUID'] = UUIDTools::UUID.random_create().to_s # should be unique 145 | payload['PayloadOrganization'] = self.organization 146 | payload 147 | end 148 | end 149 | end --------------------------------------------------------------------------------