app-info 2.8.4 → 3.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|