apple_push_certs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 222f59f98f7017522ba67147f454abe783bdd50f
4
+ data.tar.gz: 576094ed03822ed7dc475dbe52ff73c477cbd51a
5
+ SHA512:
6
+ metadata.gz: b91d64ad310dc91b7a8c636a3ed5a44866b274bd30afbde26c36fb444acc0ec24d09966108eda9a9f0360b33b6d58e411901556edb802e01778c9839e674d6d0
7
+ data.tar.gz: 244b0195fe1b812b9fa8d1e479c4e0f857ce4d20c74c9e11ed6f29e0362be6451482d92d053938885a4a80c6d954b8be59ad5b5d0c34328f0c91fadf24c3353c
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,50 @@
1
+ # Apple Push Certs
2
+
3
+ Helps with the process of getting a certificate for the Apple Push Notification Service.
4
+
5
+ This gem:
6
+
7
+ * Creates private keys
8
+ * Creates CSRs
9
+ * Signs CSRs
10
+ * Submits signed CSRs to Apple
11
+ * Retrieves push certificates from Apple
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'apple_push_certs'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install apple_push_certs
26
+
27
+ ## Usage
28
+
29
+ ### Create a New Certificate
30
+
31
+ include 'ApplePushCerts' # for the sake of terseness
32
+
33
+ key = Customer::Key.new
34
+ csr = Customer::CSR.new key
35
+
36
+ signed_csr = SignedCSR.new 'my_signing_cert.p12', 'my_cert_password', csr
37
+
38
+ certificate = Certificate.new signed_csr, 'apple_id', 'password'
39
+
40
+
41
+ ### Renew an Existing Certificate
42
+
43
+ key = [existing key]
44
+ certificate = [existing certificate] # should be of type OpenSSL::X509::Certificate
45
+
46
+ csr = Customer::CSR.new key
47
+
48
+ signed_csr = SignedCSR.new 'my_signing_cert.p12', 'my_cert_password', csr
49
+
50
+ certificate = Certificate.renew certificate, signed_csr, 'apple_id', 'password'
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'apple_push_certs/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "apple_push_certs"
8
+ spec.version = ApplePushCerts::VERSION
9
+ spec.authors = ["Taylor Boyko"]
10
+ spec.email = ["taylor@wrprojects.com"]
11
+ spec.description = %q{Generate Apple Push CSRs and Retrieve Certificates from the Apple Push Certificates Portal}
12
+ spec.summary = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_dependency 'plist'
25
+ spec.add_dependency 'multipart-post'
26
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'apple_push_certs', '**', '*.rb')].each do |file|
2
+ require file
3
+ end
@@ -0,0 +1,129 @@
1
+ require 'net/http'
2
+ require 'net/http/post/multipart'
3
+ require 'json'
4
+ require 'openssl'
5
+
6
+ module ApplePushCerts
7
+ class Certificate < OpenSSL::X509::Certificate
8
+
9
+ def initialize(signed_csr, apple_id, password)
10
+ cookie = self.class.sign_in(apple_id, password)
11
+ cert = self.class.generate_cert(cookie, signed_csr)
12
+
13
+ super cert
14
+ end
15
+
16
+ def self.renew(certificate, signed_csr, apple_id, password)
17
+ cookie = sign_in(apple_id, password)
18
+ cert = renew_cert(certificate, cookie, signed_csr)
19
+ end
20
+
21
+ private
22
+
23
+ # returns a cookie
24
+ def self.sign_in(apple_id, password)
25
+ uri = '/cgi-bin/WebObjects/DSAuthWeb.woa/wa/login?appIdKey=3fbfc9ad8dfedeb78be1d37f6458e72adc3160d1ad5b323a9e5c5eb2f8e7e3e2&rv=2'
26
+
27
+ http = Net::HTTP.new('daw.apple.com', 443)
28
+ http.use_ssl = true
29
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
30
+
31
+ # get form validation token
32
+
33
+ res = http.get(uri)
34
+ form_url = res.body.match(/action=\"(\/cgi\-bin[^\"]+)\"/)[1]
35
+ wosid = form_url.match(/([^\/]+)\/[^\/]+$/)[1]
36
+
37
+ # sign in
38
+
39
+ data = "theAccountName=#{apple_id}&theAccountPW=#{password}&wosid=#{wosid}"
40
+ headers = {
41
+ 'Content-Type' => 'application/x-www-form-urlencoded'
42
+ }
43
+
44
+ res = http.post(form_url, data, headers)
45
+
46
+ cookie = res.response['set-cookie']
47
+ raise AppleSignInError, "incorrect apple id or password" unless cookie
48
+
49
+ cookie
50
+ end
51
+
52
+ # returns a string containing the newly generated certificate
53
+ def self.generate_cert(cookie, signed_csr)
54
+ http = Net::HTTP.new('identity.apple.com', 443)
55
+ http.use_ssl = true
56
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
57
+
58
+ headers = {
59
+ 'Cookie' => cookie
60
+ }
61
+
62
+ # upload csr
63
+
64
+ scsr_io = StringIO.new signed_csr
65
+ req = Net::HTTP::Post::Multipart.new '/pushcert/create', {upfile: UploadIO.new(scsr_io, 'application/octet-stream')}, headers
66
+
67
+ res = http.request(req)
68
+ res_json = JSON.parse(res.body)
69
+ serial = res_json['CertSN']
70
+
71
+ raise StandardError, "apple did not respond with a serial number. response: #{res.body}" unless serial
72
+
73
+ # download cert
74
+
75
+ download_certificate(serial, http, headers)
76
+ end
77
+
78
+ def self.renew_cert(certificate, cookie, signed_csr)
79
+
80
+ # connect
81
+
82
+ http = Net::HTTP.new('identity.apple.com', 443)
83
+ http.use_ssl = true
84
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
85
+
86
+ headers = {
87
+ 'Cookie' => cookie
88
+ }
89
+
90
+ # get existing cert listing
91
+
92
+ res = http.get("/pushcert/userinfo", headers)
93
+ res_json = JSON.parse res.body
94
+
95
+ token = (certificate.subject.to_s.match /APSP:([^\/]+)/)[1]
96
+ raise AppleCertificateNotFoundError, "apple does not have any certificates on file" unless res_json['CertDetails']
97
+ cert_json = res_json['CertDetails'].select { |c| c['CertificateToken'].match token }.first
98
+ raise AppleCertificateNotFoundError, "apple does not have that certificate on file" unless cert_json
99
+
100
+ # upload csr
101
+
102
+ uri = "/pushcert/renew?csn=#{URI.escape(cert_json['SerialNumber'])}"
103
+ uri << "&idn=#{URI.escape(cert_json['IssuerDN'])}"
104
+ uri << "&ctkn=#{URI.escape(cert_json['CertificateToken'])}"
105
+
106
+ scsr_io = StringIO.new signed_csr
107
+ req = Net::HTTP::Post::Multipart.new uri, {upfile: UploadIO.new(scsr_io, 'application/octet-stream')}, headers
108
+
109
+ res = http.request(req)
110
+ res_json = JSON.parse(res.body)
111
+ serial = res_json['CertSN']
112
+
113
+ # download new cert
114
+
115
+ download_certificate(serial, http, headers)
116
+ end
117
+
118
+ # returns a certificate
119
+ def self.download_certificate(serial, http, headers)
120
+ res = http.get("/pushcert/download?csn=#{serial}", headers)
121
+
122
+ raise AppleCertificateNotFoundError, "apple did not respond with a certificate" unless res.body.match(/BEGIN CERTIFICATE/)
123
+
124
+ OpenSSL::X509::Certificate.new res.body
125
+ end
126
+
127
+
128
+ end
129
+ end
@@ -0,0 +1,6 @@
1
+ module ApplePushCerts
2
+ CONFIG = {
3
+ apple_intermediate_certificate_path: File.expand_path('../../../certificates/AppleWWDRCA.cer', __FILE__),
4
+ apple_root_certificate_path: File.expand_path('../../../certificates/AppleIncRootCertificate.cer', __FILE__)
5
+ }
6
+ end
@@ -0,0 +1,40 @@
1
+ require 'openssl'
2
+
3
+ module ApplePushCerts
4
+ module Customer
5
+ class CSR < OpenSSL::X509::Request
6
+
7
+ def initialize(key=nil)
8
+ key ||= Key.new
9
+ csr = OpenSSL::X509::Request.new
10
+
11
+ options = {
12
+ country: '',
13
+ state: '',
14
+ city: '',
15
+ organization: '',
16
+ department: '',
17
+ common_name: '',
18
+ email: ''
19
+ }
20
+
21
+ csr.version = 0
22
+ csr.subject = OpenSSL::X509::Name.new([
23
+ ['C', options[:country], OpenSSL::ASN1::PRINTABLESTRING],
24
+ ['ST', options[:state], OpenSSL::ASN1::PRINTABLESTRING],
25
+ ['L', options[:city], OpenSSL::ASN1::PRINTABLESTRING],
26
+ ['O', options[:organization], OpenSSL::ASN1::UTF8STRING],
27
+ ['OU', options[:department], OpenSSL::ASN1::UTF8STRING],
28
+ ['CN', options[:common_name], OpenSSL::ASN1::UTF8STRING],
29
+ ['emailAddress', options[:email], OpenSSL::ASN1::UTF8STRING]
30
+ ])
31
+
32
+ csr.public_key = key.public_key
33
+ csr.sign key, OpenSSL::Digest::SHA1.new
34
+
35
+ super csr
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ require 'openssl'
2
+
3
+ module ApplePushCerts
4
+ module Customer
5
+ class Key < OpenSSL::PKey::RSA
6
+
7
+ def initialize
8
+ super 2048
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ module ApplePushCerts
2
+ class Error < RuntimeError
3
+ end
4
+
5
+ class AppleSignInError < Error
6
+ end
7
+
8
+ class AppleCertificateNotFoundError < Error
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ require 'base64'
2
+ require 'plist'
3
+
4
+ module ApplePushCerts
5
+ class SignedCSR < String
6
+
7
+ def initialize(vendor_p12_path, p12_password, customer_csr)
8
+ vendor_p12 = OpenSSL::PKCS12.new(File.open(vendor_p12_path).read, p12_password)
9
+
10
+ csr_der = customer_csr.to_der
11
+ signed_csr_der = vendor_p12.key.sign(OpenSSL::Digest::SHA1.new, csr_der)
12
+
13
+ request = {
14
+ 'PushCertRequestCSR' => Base64.encode64(csr_der),
15
+ 'PushCertCertificateChain' => certificate_chain(vendor_p12),
16
+ 'PushCertSignature' => Base64.encode64(signed_csr_der)
17
+ }
18
+
19
+ # Apple doesn't like tab characters in the plist
20
+ plist = Plist::Emit.dump(request).gsub(/\t/,'')
21
+
22
+ signed_csr = Base64.encode64 plist
23
+
24
+ super signed_csr
25
+ end
26
+
27
+ private
28
+
29
+ def certificate_chain(vendor_p12)
30
+ apple_root = path_to_pem CONFIG[:apple_root_certificate_path]
31
+ apple_intermediate = path_to_pem CONFIG[:apple_intermediate_certificate_path]
32
+ vendor_cert = vendor_p12.certificate.to_pem
33
+
34
+ [apple_root, apple_intermediate, vendor_cert].join
35
+ end
36
+
37
+ def path_to_pem(path)
38
+ cert = OpenSSL::X509::Certificate.new File.open(path).read
39
+ cert.to_pem
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module ApplePushCerts
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,15 @@
1
+ require 'apple_push_certs'
2
+
3
+ describe ApplePushCerts do
4
+
5
+ it 'has the apple root certificate' do
6
+ root_certificate = File.open ApplePushCerts::CONFIG[:apple_root_certificate_path]
7
+ root_certificate.read.length.should be > 0
8
+ end
9
+
10
+ it 'has the apple intermediate certificate' do
11
+ root_certificate = File.open ApplePushCerts::CONFIG[:apple_intermediate_certificate_path]
12
+ root_certificate.read.length.should be > 0
13
+ end
14
+
15
+ end
@@ -0,0 +1,17 @@
1
+ require 'apple_push_certs'
2
+
3
+ describe ApplePushCerts::Customer::CSR do
4
+ before do
5
+ @csr = ApplePushCerts::Customer::CSR.new
6
+ end
7
+
8
+ it 'initializes' do
9
+ @csr.should be_a_kind_of(OpenSSL::X509::Request)
10
+ end
11
+
12
+ it 'generates a csr' do
13
+ matches = @csr.to_s.match /BEGIN CERTIFICATE REQUEST/
14
+ matches.should_not eq(nil)
15
+ end
16
+
17
+ end
@@ -0,0 +1,16 @@
1
+ require 'apple_push_certs'
2
+
3
+ describe ApplePushCerts::Customer::Key do
4
+ before do
5
+ @key = ApplePushCerts::Customer::Key.new
6
+ end
7
+
8
+ it 'initializes' do
9
+ @key.should be_a_kind_of(OpenSSL::PKey::RSA)
10
+ end
11
+
12
+ it 'generates a key' do
13
+ matches = @key.to_s.match(/BEGIN RSA PRIVATE KEY/)
14
+ matches.should_not eq(nil)
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apple_push_certs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Taylor Boyko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: plist
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: multipart-post
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Generate Apple Push CSRs and Retrieve Certificates from the Apple Push
84
+ Certificates Portal
85
+ email:
86
+ - taylor@wrprojects.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - .gitignore
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - apple_push_certs.gemspec
97
+ - certificates/AppleIncRootCertificate.cer
98
+ - certificates/AppleWWDRCA.cer
99
+ - lib/apple_push_certs.rb
100
+ - lib/apple_push_certs/certificate.rb
101
+ - lib/apple_push_certs/config.rb
102
+ - lib/apple_push_certs/customer/csr.rb
103
+ - lib/apple_push_certs/customer/key.rb
104
+ - lib/apple_push_certs/errors.rb
105
+ - lib/apple_push_certs/signed_csr.rb
106
+ - lib/apple_push_certs/version.rb
107
+ - spec/apple_push_certs_spec.rb
108
+ - spec/customer_csr_spec.rb
109
+ - spec/private_key_spec.rb
110
+ homepage: ''
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.4.2
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: ''
134
+ test_files:
135
+ - spec/apple_push_certs_spec.rb
136
+ - spec/customer_csr_spec.rb
137
+ - spec/private_key_spec.rb