app-info 2.8.4 → 3.0.0.beta1
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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +12 -7
- data/.github/workflows/ci.yml +3 -1
- data/.rubocop.yml +31 -11
- data/CHANGELOG.md +30 -2
- data/Gemfile +7 -1
- data/README.md +74 -9
- data/Rakefile +11 -0
- data/app_info.gemspec +12 -3
- data/lib/app_info/aab.rb +58 -39
- data/lib/app_info/android/signature.rb +114 -0
- data/lib/app_info/android/signatures/base.rb +49 -0
- data/lib/app_info/android/signatures/info.rb +152 -0
- data/lib/app_info/android/signatures/v1.rb +59 -0
- data/lib/app_info/android/signatures/v2.rb +117 -0
- data/lib/app_info/android/signatures/v3.rb +127 -0
- data/lib/app_info/android/signatures/v4.rb +14 -0
- data/lib/app_info/apk.rb +43 -46
- data/lib/app_info/certificate.rb +181 -0
- data/lib/app_info/const.rb +41 -0
- data/lib/app_info/core_ext/object/try.rb +3 -1
- data/lib/app_info/core_ext/string/inflector.rb +2 -0
- data/lib/app_info/dsym/debug_info.rb +72 -0
- data/lib/app_info/dsym/macho.rb +55 -0
- data/lib/app_info/dsym.rb +27 -134
- data/lib/app_info/error.rb +8 -0
- data/lib/app_info/file.rb +23 -0
- data/lib/app_info/helper/archive.rb +37 -0
- data/lib/app_info/helper/file_size.rb +25 -0
- data/lib/app_info/helper/generate_class.rb +29 -0
- data/lib/app_info/helper/protobuf.rb +12 -0
- data/lib/app_info/helper/signatures.rb +229 -0
- data/lib/app_info/helper.rb +5 -126
- data/lib/app_info/info_plist.rb +14 -6
- data/lib/app_info/ipa/framework.rb +4 -4
- data/lib/app_info/ipa.rb +41 -36
- data/lib/app_info/macos.rb +34 -26
- data/lib/app_info/mobile_provision.rb +19 -30
- data/lib/app_info/pe.rb +226 -0
- data/lib/app_info/png_uncrush.rb +5 -4
- data/lib/app_info/proguard.rb +11 -17
- data/lib/app_info/protobuf/manifest.rb +16 -9
- data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
- data/lib/app_info/protobuf/models/README.md +7 -0
- data/lib/app_info/protobuf/models/Resources_pb.rb +2 -0
- data/lib/app_info/protobuf/resources.rb +5 -5
- data/lib/app_info/version.rb +1 -1
- data/lib/app_info.rb +91 -42
- metadata +46 -35
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo
|
4
|
+
module Android
|
5
|
+
# Android Signature
|
6
|
+
#
|
7
|
+
# Support digest and length:
|
8
|
+
#
|
9
|
+
# RSA:1024、2048、4096、8192、16384
|
10
|
+
# EC:NIST P-256、P-384、P-521
|
11
|
+
# DSA:1024、2048、3072
|
12
|
+
module Signature
|
13
|
+
class VersionError < Error; end
|
14
|
+
class SecurityError < Error; end
|
15
|
+
class NotFoundError < NotFoundError; end
|
16
|
+
|
17
|
+
module Version
|
18
|
+
V1 = 1
|
19
|
+
V2 = 2
|
20
|
+
V3 = 3
|
21
|
+
V4 = 4
|
22
|
+
end
|
23
|
+
|
24
|
+
# All registerd verions to verify
|
25
|
+
#
|
26
|
+
# key is the version
|
27
|
+
# value is the class
|
28
|
+
@versions = {}
|
29
|
+
|
30
|
+
class << self
|
31
|
+
# Verify Android Signature
|
32
|
+
#
|
33
|
+
# @example Get unverified v1 certificates, verified v2 certificates,
|
34
|
+
# and not found v3 certificate
|
35
|
+
#
|
36
|
+
# signature.versions(parser)
|
37
|
+
# # => [
|
38
|
+
# # {
|
39
|
+
# # version: 1,
|
40
|
+
# # verified: false,
|
41
|
+
# # certificates: [<AppInfo::Certificate>, ...],
|
42
|
+
# # verifier: AppInfo::Androig::Signature
|
43
|
+
# # },
|
44
|
+
# # {
|
45
|
+
# # version: 2,
|
46
|
+
# # verified: false,
|
47
|
+
# # certificates: [<AppInfo::Certificate>, ...],
|
48
|
+
# # verifier: AppInfo::Androig::Signature
|
49
|
+
# # },
|
50
|
+
# # {
|
51
|
+
# # version: 3
|
52
|
+
# # }
|
53
|
+
# # ]
|
54
|
+
# @todo version 4 no implantation yet
|
55
|
+
# @param [AppInfo::File] parser
|
56
|
+
# @param [Version, Integer] min_version
|
57
|
+
# @return [Array<Hash>] versions
|
58
|
+
def verify(parser, min_version: Version::V4)
|
59
|
+
min_version = min_version.to_i if min_version.is_a?(String)
|
60
|
+
if min_version && min_version > Version::V4
|
61
|
+
raise VersionError,
|
62
|
+
"No signature found in #{min_version} scheme or newer for android file"
|
63
|
+
end
|
64
|
+
|
65
|
+
if min_version.zero?
|
66
|
+
raise VersionError,
|
67
|
+
"Unkonwn version: #{min_version}, avaiables in 1/2/3 and 4 (no implantation yet)"
|
68
|
+
end
|
69
|
+
|
70
|
+
# try full version signatures if min_version is nil
|
71
|
+
versions = min_version.downto(Version::V1).each_with_object([]) do |version, signatures|
|
72
|
+
next unless kclass = fetch(version)
|
73
|
+
|
74
|
+
data = { version: version }
|
75
|
+
begin
|
76
|
+
verifier = kclass.verify(parser)
|
77
|
+
data[:verified] = verifier.verified
|
78
|
+
data[:certificates] = verifier.certificates
|
79
|
+
data[:verifier] = verifier
|
80
|
+
rescue SecurityError, NotFoundError
|
81
|
+
# not this version, try the low version
|
82
|
+
ensure
|
83
|
+
signatures << data
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
versions.sort_by { |entry| entry[:version] }
|
88
|
+
end
|
89
|
+
|
90
|
+
def registered
|
91
|
+
@versions.keys
|
92
|
+
end
|
93
|
+
|
94
|
+
def register(version, verifier)
|
95
|
+
@versions[version] = verifier
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch(version)
|
99
|
+
@versions[version]
|
100
|
+
end
|
101
|
+
|
102
|
+
def exist?(version)
|
103
|
+
@versions.key?(version)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
UINT32_MAX_VALUE = 2_147_483_647
|
108
|
+
UINT32_SIZE = 4
|
109
|
+
UINT64_SIZE = 8
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
require 'app_info/android/signatures/base'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_info/android/signatures/info'
|
4
|
+
|
5
|
+
module AppInfo::Android::Signature
|
6
|
+
class Base
|
7
|
+
def self.verify(parser)
|
8
|
+
instance = new(parser)
|
9
|
+
instance.verify
|
10
|
+
instance
|
11
|
+
end
|
12
|
+
|
13
|
+
DESCRIPTION = 'APK Signature Scheme'
|
14
|
+
|
15
|
+
attr_reader :verified
|
16
|
+
|
17
|
+
def initialize(parser)
|
18
|
+
@parser = parser
|
19
|
+
@verified = false
|
20
|
+
end
|
21
|
+
|
22
|
+
# @abstract Subclass and override {#verify} to implement
|
23
|
+
def verify
|
24
|
+
raise NotImplementedError, ".#{__method__} method implantation required in #{self.class}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# @abstract Subclass and override {#certificates} to implement
|
28
|
+
def certificates
|
29
|
+
raise NotImplementedError, ".#{__method__} method implantation required in #{self.class}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def scheme
|
33
|
+
"v#{version}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def description
|
37
|
+
"#{DESCRIPTION} #{scheme}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def logger
|
41
|
+
@parser.logger
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
require 'app_info/android/signatures/v1'
|
47
|
+
require 'app_info/android/signatures/v2'
|
48
|
+
require 'app_info/android/signatures/v3'
|
49
|
+
require 'app_info/android/signatures/v4'
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module AppInfo::Android::Signature
|
7
|
+
# APK signature scheme signurate info
|
8
|
+
#
|
9
|
+
# FORMAT:
|
10
|
+
# OFFSET DATA TYPE DESCRIPTION
|
11
|
+
# * @+0 bytes uint64: size in bytes (excluding this field)
|
12
|
+
# * @+8 bytes payload
|
13
|
+
# * @-24 bytes uint64: size in bytes (same as the one above)
|
14
|
+
# * @-16 bytes uint128: magic value
|
15
|
+
class Info
|
16
|
+
include AppInfo::Helper::IOBlock
|
17
|
+
|
18
|
+
# Signature block information
|
19
|
+
SIG_SIZE_OF_BLOCK_SIZE = 8
|
20
|
+
SIG_MAGIC_BLOCK_SIZE = 16
|
21
|
+
SIG_BLOCK_MIN_SIZE = 32
|
22
|
+
|
23
|
+
# Magic value: APK Sig Block 42
|
24
|
+
SIG_MAGIC = [
|
25
|
+
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69,
|
26
|
+
0x67, 0x20, 0x42, 0x6c, 0x6f, 0x63,
|
27
|
+
0x6b, 0x20, 0x34, 0x32
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
attr_reader :total_size, :pairs, :magic, :logger
|
31
|
+
|
32
|
+
def initialize(version, parser, logger)
|
33
|
+
@version = version
|
34
|
+
@parser = parser
|
35
|
+
@logger = logger
|
36
|
+
|
37
|
+
pares_signatures_pairs
|
38
|
+
end
|
39
|
+
|
40
|
+
# Find singers
|
41
|
+
#
|
42
|
+
# FORMAT:
|
43
|
+
# OFFSET DATA TYPE DESCRIPTION
|
44
|
+
# * @+0 bytes uint64: size in bytes
|
45
|
+
# * @+8 bytes payload block
|
46
|
+
# * @+0 bytes uint32: id
|
47
|
+
# * @+4 bytes payload: value
|
48
|
+
def signers(block_id)
|
49
|
+
count = 0
|
50
|
+
until @pairs.eof?
|
51
|
+
left_bytes = left_bytes_check(
|
52
|
+
@pairs, UINT64_SIZE, NotFoundError,
|
53
|
+
"Insufficient data to read size of APK Signing Block ##{count}"
|
54
|
+
)
|
55
|
+
|
56
|
+
pair_buf = @pairs.read(UINT64_SIZE)
|
57
|
+
pair_size = pair_buf.unpack1('Q')
|
58
|
+
if pair_size < UINT32_SIZE || pair_size > UINT32_MAX_VALUE
|
59
|
+
raise NotFoundError,
|
60
|
+
"APK Signing Block ##{count} size out of range: #{pair_size} > #{UINT32_MAX_VALUE}"
|
61
|
+
end
|
62
|
+
|
63
|
+
if pair_size > left_bytes
|
64
|
+
raise NotFoundError,
|
65
|
+
"APK Signing Block ##{count} size out of range: #{pair_size} > #{left_bytes}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# fetch next signer block position
|
69
|
+
next_pos = @pairs.pos + pair_size.to_i
|
70
|
+
|
71
|
+
id_block = @pairs.read(UINT32_SIZE)
|
72
|
+
id_bytes = id_block.unpack('C*')
|
73
|
+
if id_bytes == block_id
|
74
|
+
logger.debug "Signature block id v#{@version} scheme (0x#{id_block.unpack1('H*')}) found"
|
75
|
+
value = @pairs.read(pair_size - UINT32_SIZE)
|
76
|
+
return StringIO.new(value)
|
77
|
+
end
|
78
|
+
|
79
|
+
@pairs.seek(next_pos)
|
80
|
+
count += 1
|
81
|
+
end
|
82
|
+
|
83
|
+
block_id_hex = block_id.reverse.pack('C*').unpack1('H*')
|
84
|
+
raise NotFoundError, "Not found block id 0x#{block_id_hex} in APK Signing Block."
|
85
|
+
end
|
86
|
+
|
87
|
+
def zip64?
|
88
|
+
zip_io.zip64_file?(start_buffer)
|
89
|
+
end
|
90
|
+
|
91
|
+
def pares_signatures_pairs
|
92
|
+
block = signature_block
|
93
|
+
block.rewind
|
94
|
+
# get pairs size
|
95
|
+
@total_size = block.size - (SIG_SIZE_OF_BLOCK_SIZE + SIG_MAGIC_BLOCK_SIZE)
|
96
|
+
|
97
|
+
# get pairs block
|
98
|
+
@pairs = StringIO.new(block.read(@total_size))
|
99
|
+
|
100
|
+
# get magic value
|
101
|
+
block.seek(block.pos + SIG_SIZE_OF_BLOCK_SIZE)
|
102
|
+
@magic = block.read(SIG_MAGIC_BLOCK_SIZE)
|
103
|
+
end
|
104
|
+
|
105
|
+
def signature_block
|
106
|
+
@signature_block ||= lambda {
|
107
|
+
logger.debug "cdir_offset: #{cdir_offset}"
|
108
|
+
|
109
|
+
file_io.seek(cdir_offset - (Info::SIG_MAGIC_BLOCK_SIZE + Info::SIG_SIZE_OF_BLOCK_SIZE))
|
110
|
+
footer_block = file_io.read(Info::SIG_SIZE_OF_BLOCK_SIZE)
|
111
|
+
if footer_block.size < Info::SIG_SIZE_OF_BLOCK_SIZE
|
112
|
+
raise NotFoundError, "APK Signing Block size out of range: #{footer_block.size}"
|
113
|
+
end
|
114
|
+
|
115
|
+
footer = footer_block.unpack1('Q')
|
116
|
+
total_size = footer
|
117
|
+
offset = cdir_offset - total_size - Info::SIG_SIZE_OF_BLOCK_SIZE
|
118
|
+
raise NotFoundError, "APK Signing Block offset out of range: #{offset}" if offset.negative?
|
119
|
+
|
120
|
+
file_io.seek(offset)
|
121
|
+
header = file_io.read(Info::SIG_SIZE_OF_BLOCK_SIZE).unpack1('Q')
|
122
|
+
|
123
|
+
if header != footer
|
124
|
+
raise NotFoundError,
|
125
|
+
"APK Signing Block header and footer mismatch: #{header} != #{footer}"
|
126
|
+
end
|
127
|
+
|
128
|
+
io = file_io.read(total_size)
|
129
|
+
StringIO.new(io)
|
130
|
+
}.call
|
131
|
+
end
|
132
|
+
|
133
|
+
def cdir_offset
|
134
|
+
@cdir_offset ||= lambda {
|
135
|
+
eocd_buffer = zip_io.get_e_o_c_d(start_buffer)
|
136
|
+
eocd_buffer[12..16].unpack1('V')
|
137
|
+
}.call
|
138
|
+
end
|
139
|
+
|
140
|
+
def start_buffer
|
141
|
+
@start_buffer ||= zip_io.start_buf(file_io)
|
142
|
+
end
|
143
|
+
|
144
|
+
def zip_io
|
145
|
+
@zip_io ||= @parser.zip
|
146
|
+
end
|
147
|
+
|
148
|
+
def file_io
|
149
|
+
@file_io ||= File.open(@parser.file, 'rb')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo::Android::Signature
|
4
|
+
# Android v1 Signature
|
5
|
+
class V1 < Base
|
6
|
+
DESCRIPTION = 'JAR signing'
|
7
|
+
|
8
|
+
PKCS7_HEADER = [0x30, 0x82].freeze
|
9
|
+
|
10
|
+
attr_reader :certificates, :signatures
|
11
|
+
|
12
|
+
def version
|
13
|
+
Version::V1
|
14
|
+
end
|
15
|
+
|
16
|
+
def description
|
17
|
+
DESCRIPTION
|
18
|
+
end
|
19
|
+
|
20
|
+
def verify
|
21
|
+
@signatures = fetch_signatures
|
22
|
+
@certificates = fetch_certificates
|
23
|
+
|
24
|
+
raise NotFoundError, 'Not found certificates' if @certificates.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def fetch_signatures
|
30
|
+
case @parser
|
31
|
+
when AppInfo::APK
|
32
|
+
signatures_from(@parser.apk)
|
33
|
+
when AppInfo::AAB
|
34
|
+
signatures_from(@parser)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_certificates
|
39
|
+
@signatures.each_with_object([]) do |(_, sign), obj|
|
40
|
+
next if sign.certificates.empty?
|
41
|
+
|
42
|
+
obj << AppInfo::Certificate.new(sign.certificates[0])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def signatures_from(parser)
|
47
|
+
signs = {}
|
48
|
+
parser.each_file do |path, data|
|
49
|
+
# find META-INF/xxx.{RSA|DSA|EC}
|
50
|
+
next unless path =~ %r{^META-INF/} && data.unpack('CC') == PKCS7_HEADER
|
51
|
+
|
52
|
+
signs[path] = OpenSSL::PKCS7.new(data)
|
53
|
+
end
|
54
|
+
signs
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
register(Version::V1, V1)
|
59
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo::Android::Signature
|
4
|
+
# Android v2 Signature
|
5
|
+
#
|
6
|
+
# FULL FORMAT:
|
7
|
+
# OFFSET DATA TYPE DESCRIPTION
|
8
|
+
# * @+0 bytes uint32: signer size in bytes
|
9
|
+
# * @+4 bytes payload signer block
|
10
|
+
# * @+0 bytes unit32: signed data size in bytes
|
11
|
+
# * @+4 bytes payload signed data block
|
12
|
+
# * @+0 bytes unit32: digests with size in bytes
|
13
|
+
# * @+0 bytes unit32: digests with size in bytes
|
14
|
+
# * @+X bytes unit32: signatures with size in bytes
|
15
|
+
# * @+X+4 bytes payload signed data block
|
16
|
+
# * @+Y bytes unit32: public key with size in bytes
|
17
|
+
# * @+Y+4 bytes payload signed data block
|
18
|
+
class V2 < Base
|
19
|
+
include AppInfo::Helper::IOBlock
|
20
|
+
include AppInfo::Helper::Signatures
|
21
|
+
include AppInfo::Helper::Algorithm
|
22
|
+
|
23
|
+
# V2 Signature ID 0x7109871a
|
24
|
+
BLOCK_ID = [0x1a, 0x87, 0x09, 0x71].freeze
|
25
|
+
|
26
|
+
attr_reader :certificates, :digests
|
27
|
+
|
28
|
+
def version
|
29
|
+
Version::V2
|
30
|
+
end
|
31
|
+
|
32
|
+
# Verify
|
33
|
+
# @todo verified signatures
|
34
|
+
def verify
|
35
|
+
signers_block = singers_block(BLOCK_ID)
|
36
|
+
@certificates, @digests = verified_certs(signers_block, verify: true)
|
37
|
+
# @verified = true
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def verified_certs(signers_block, verify:)
|
43
|
+
unless (signers = length_prefix_block(signers_block))
|
44
|
+
raise SecurityError, 'Not found signers'
|
45
|
+
end
|
46
|
+
|
47
|
+
certificates = []
|
48
|
+
content_digests = {}
|
49
|
+
loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
|
50
|
+
signer_certs, signer_digests = extract_signer_data(signer, verify: verify)
|
51
|
+
certificates.concat(signer_certs)
|
52
|
+
content_digests.merge!(signer_digests)
|
53
|
+
end
|
54
|
+
raise SecurityError, 'No signers found' if certificates.empty?
|
55
|
+
|
56
|
+
[certificates, content_digests]
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_signer_data(signer, verify:)
|
60
|
+
# raw data
|
61
|
+
signed_data = length_prefix_block(signer)
|
62
|
+
signatures = length_prefix_block(signer)
|
63
|
+
public_key = length_prefix_block(signer, raw: true)
|
64
|
+
|
65
|
+
# FIXME: extract code below and re-organizate
|
66
|
+
|
67
|
+
algorithems = signature_algorithms(signatures)
|
68
|
+
raise SecurityError, 'No signatures found' if verify && algorithems.empty?
|
69
|
+
|
70
|
+
# find best algorithem to verify signed data with public key and signature
|
71
|
+
unless best_algorithem = best_algorithem(algorithems)
|
72
|
+
raise SecurityError, 'No supported signatures found'
|
73
|
+
end
|
74
|
+
|
75
|
+
algorithems_digest = best_algorithem[:digest]
|
76
|
+
signature = best_algorithem[:signature]
|
77
|
+
|
78
|
+
pkey = OpenSSL::PKey.read(public_key)
|
79
|
+
digest = OpenSSL::Digest.new(algorithems_digest)
|
80
|
+
verified = pkey.verify(digest, signature, signed_data.string)
|
81
|
+
raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
|
82
|
+
|
83
|
+
# verify algorithm ID full equal (and sort) between digests and signature
|
84
|
+
digests = length_prefix_block(signed_data)
|
85
|
+
content_digests = signed_data_digests(digests)
|
86
|
+
content_digest = content_digests[algorithems_digest]&.fetch(:content)
|
87
|
+
|
88
|
+
unless content_digest
|
89
|
+
raise SecurityError,
|
90
|
+
'Signature algorithms don\'t match between digests and signatures records'
|
91
|
+
end
|
92
|
+
|
93
|
+
previous_digest = content_digests.fetch(algorithems_digest)
|
94
|
+
content_digests[algorithems_digest] = content_digest
|
95
|
+
if previous_digest && previous_digest[:content] != content_digest
|
96
|
+
raise SecurityError,
|
97
|
+
'Signature algorithms don\'t match between digests and signatures records'
|
98
|
+
end
|
99
|
+
|
100
|
+
certificates = length_prefix_block(signed_data)
|
101
|
+
certs = signed_data_certs(certificates)
|
102
|
+
raise SecurityError, 'No certificates listed' if certs.empty?
|
103
|
+
|
104
|
+
main_cert = certs[0]
|
105
|
+
if main_cert.public_key.to_der != pkey.to_der
|
106
|
+
raise SecurityError, 'Public key mismatch between certificate and signature record'
|
107
|
+
end
|
108
|
+
|
109
|
+
additional_attrs = length_prefix_block(signed_data)
|
110
|
+
verify_additional_attrs(additional_attrs, certs)
|
111
|
+
|
112
|
+
[certs, content_digests]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
register(Version::V2, V2)
|
117
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo::Android::Signature
|
4
|
+
# Android v3 Signature
|
5
|
+
#
|
6
|
+
# FULL FORMAT:
|
7
|
+
# OFFSET DATA TYPE DESCRIPTION
|
8
|
+
# * @+0 bytes uint32: signer size in bytes
|
9
|
+
# * @+4 bytes payload signer block
|
10
|
+
# * @+0 bytes unit32: signed data size in bytes
|
11
|
+
# * @+4 bytes payload signed data block
|
12
|
+
# * @+0 bytes unit32: digests with size in bytes
|
13
|
+
# * @+0 bytes unit32: digests with size in bytes
|
14
|
+
# * @+W bytes unit32: minSDK
|
15
|
+
# * @+X+4 bytes unit32: maxSDK
|
16
|
+
# * @+Y+4 bytes unit32: signatures with size in bytes
|
17
|
+
# * @+Y+4 bytes payload signed data block
|
18
|
+
# * @+Z bytes unit32: public key with size in bytes
|
19
|
+
# * @+Z+4 bytes payload signed data block
|
20
|
+
class V3 < Base
|
21
|
+
include AppInfo::Helper::IOBlock
|
22
|
+
include AppInfo::Helper::Signatures
|
23
|
+
include AppInfo::Helper::Algorithm
|
24
|
+
|
25
|
+
# V3 Signature ID 0xf05368c0
|
26
|
+
V3_BLOCK_ID = [0xc0, 0x68, 0x53, 0xf0].freeze
|
27
|
+
|
28
|
+
# V3.1 Signature ID 0x1b93ad61
|
29
|
+
V3_1_BLOCK_ID = [0x61, 0xad, 0x93, 0x1b].freeze
|
30
|
+
|
31
|
+
attr_reader :certificates, :digests
|
32
|
+
|
33
|
+
def version
|
34
|
+
Version::V3
|
35
|
+
end
|
36
|
+
|
37
|
+
def verify
|
38
|
+
begin
|
39
|
+
signers_block = singers_block(V3_1_BLOCK_ID)
|
40
|
+
rescue NotFoundError
|
41
|
+
signers_block = singers_block(V3_BLOCK_ID)
|
42
|
+
end
|
43
|
+
|
44
|
+
@certificates, @digests = verified_certs(signers_block)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def verified_certs(signers_block)
|
50
|
+
unless (signers = length_prefix_block(signers_block))
|
51
|
+
raise SecurityError, 'Not found signers'
|
52
|
+
end
|
53
|
+
|
54
|
+
certificates = []
|
55
|
+
content_digests = {}
|
56
|
+
loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
|
57
|
+
signer_certs, signer_digests = extract_signer_data(signer)
|
58
|
+
certificates.concat(signer_certs)
|
59
|
+
content_digests.merge!(signer_digests)
|
60
|
+
end
|
61
|
+
raise SecurityError, 'No signers found' if certificates.empty?
|
62
|
+
|
63
|
+
[certificates, content_digests]
|
64
|
+
end
|
65
|
+
|
66
|
+
def extract_signer_data(signer)
|
67
|
+
# raw data
|
68
|
+
signed_data = length_prefix_block(signer)
|
69
|
+
|
70
|
+
# TODO: verify min_sdk and max_sdk
|
71
|
+
min_sdk = signer.read(UINT32_SIZE)
|
72
|
+
max_sdk = signer.read(UINT32_SIZE)
|
73
|
+
|
74
|
+
signatures = length_prefix_block(signer)
|
75
|
+
public_key = length_prefix_block(signer, raw: true)
|
76
|
+
|
77
|
+
algorithems = signature_algorithms(signatures)
|
78
|
+
raise SecurityError, 'No signatures found' if algorithems.empty?
|
79
|
+
|
80
|
+
# find best algorithem to verify signed data with public key and signature
|
81
|
+
unless best_algorithem = best_algorithem(algorithems)
|
82
|
+
raise SecurityError, 'No supported signatures found'
|
83
|
+
end
|
84
|
+
|
85
|
+
algorithems_digest = best_algorithem[:digest]
|
86
|
+
signature = best_algorithem[:signature]
|
87
|
+
|
88
|
+
pkey = OpenSSL::PKey.read(public_key)
|
89
|
+
digest = OpenSSL::Digest.new(algorithems_digest)
|
90
|
+
verified = pkey.verify(digest, signature, signed_data.string)
|
91
|
+
raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
|
92
|
+
|
93
|
+
# verify algorithm ID full equal (and sort) between digests and signature
|
94
|
+
digests = length_prefix_block(signed_data)
|
95
|
+
content_digests = signed_data_digests(digests)
|
96
|
+
content_digest = content_digests[algorithems_digest]&.fetch(:content)
|
97
|
+
|
98
|
+
unless content_digest
|
99
|
+
raise SecurityError,
|
100
|
+
'Signature algorithms don\'t match between digests and signatures records'
|
101
|
+
end
|
102
|
+
|
103
|
+
previous_digest = content_digests.fetch(algorithems_digest)
|
104
|
+
content_digests[algorithems_digest] = content_digest
|
105
|
+
if previous_digest && previous_digest[:content] != content_digest
|
106
|
+
raise SecurityError,
|
107
|
+
'Signature algorithms don\'t match between digests and signatures records'
|
108
|
+
end
|
109
|
+
|
110
|
+
certificates = length_prefix_block(signed_data)
|
111
|
+
certs = signed_data_certs(certificates)
|
112
|
+
raise SecurityError, 'No certificates listed' if certs.empty?
|
113
|
+
|
114
|
+
main_cert = certs[0]
|
115
|
+
if main_cert.public_key.to_der != pkey.to_der
|
116
|
+
raise SecurityError, 'Public key mismatch between certificate and signature record'
|
117
|
+
end
|
118
|
+
|
119
|
+
additional_attrs = length_prefix_block(signed_data)
|
120
|
+
verify_additional_attrs(additional_attrs, certs)
|
121
|
+
|
122
|
+
[certs, content_digests]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
register(Version::V3, V3)
|
127
|
+
end
|