まずは公式 Web サイトのV2 署名の紹介をご覧ください。
APK 署名スキーム v2 は、APK の保護された部分に加えられたすべての変更を検出できるフルファイル署名スキームであり、それによって検証を高速化し、整合性保証を強化するのに役立ちます。
APK 署名スキーム v2 で署名する場合、APK 署名ブロックは APK ファイルの「ZIP Central Directory」セクションの直前と直前に挿入されます。APK 署名ブロック内では、v2 署名と署名者の ID 情報が APK 署名スキーム v2 ブロックに保存されます。
APK 署名スキーム v2 は Android 7.0 (Nougat) で導入されました。APK を Android 6.0 (Marshmallow) 以前のデバイスにインストールできるようにするには、v2 スキームを使用して署名する前に、JAR 署名機能を使用して APK に署名する必要があります。
APK 署名ブロック
v1 APK 形式との下位互換性を維持するために、v2 以降の APK 署名は、APK 署名スキーム v2 をサポートするために導入された新しいコンテナである「APK 署名ブロック」に保存されます。APK ファイルでは、「APK Signature Block」はファイル末尾の「ZIP Central Directory」セクションの直前とその直前にあります。
このチャンクには複数の ID と値のペアが含まれており、APK 内でチャンクを見つけやすい方法でパッケージ化されています。APK の v2 署名は ID と値のペアとして保存されます。ID は 0x7109871a です。
フォーマット
「APK 署名ブロック」の形式は次のとおりです (すべての数値フィールドはリトルエンディアンです)。
- ブロックのサイズ (バイト単位) (このフィールドを除く) (uint64)
- uint64 の長さがプレフィックスとして付けられた ID と値のペアのシーケンス:
- ID (uint32)
- value (可変長: 「ID-value」ペアの長さ - 4 バイト)
- ブロックのサイズ (バイト単位) - 最初のフィールドと同じ (uint64)
- マジック「APK署名ブロック42」(16バイト)
APK を解析するとき、ファイルの最後にある「ZIP セントラル ディレクトリの終わり」レコードを見つけて、次にそのレコードから「セントラル ディレクトリ」の開始オフセットを読み取ることで、「ZIP セントラル ディレクトリ」の先頭が見つかります。 。マジック値により、「APK 署名ブロック」が「中央ディレクトリ」よりも前にある可能性があることがすぐに判断できます。次に、ブロックのサイズの値を通じて、ファイル内のブロックの開始位置を効率的に見つけることができます。
不明な ID を持つ ID と値のペアは、このチャンクを解釈するときに無視されます。
APK 署名スキーム v2 チャンク化された
APK は 1 つ以上の署名者/アイデンティティによって署名され、それぞれが署名キーで表されます。この情報は「APK 署名スキーム v2 チャンク」として保存されます。署名者ごとに、次の情報が保存されます。
- (署名アルゴリズム、ダイジェスト、署名) タプル。ダイジェストは、署名検証と APK コンテンツの整合性チェックを分離できるように保存されます。
- 署名者の ID を表す X.509 証明書チェーン。
- キーと値のペアの形式の追加プロパティ。
署名者ごとに、受け取ったリストのサポートされている署名を使用して APK が検証されます。不明な署名アルゴリズムによる署名は無視されます。サポートされている署名が複数ある場合、どれを使用するかを選択するのは各実装の責任です。これにより、将来的には、下位互換性のある方法で、より安全な署名方法を導入することが可能になります。推奨されるアプローチは、最高のセキュリティ署名を検証することです。
フォーマット
「APK Signature Scheme v2 Chunk」は、ID 0x7109871a の「APK Signature Chunk」に格納されています。
「APK 署名スキーム v2 チャンク」の形式は次のとおりです (すべての数値はリトルエンディアンのバイトオーダーであり、すべての長さの接頭辞が付いているフィールドは長さとして uint32 値を使用します)。
- 長さプレフィックス付き署名者 (長さプレフィックス付き) シーケンス:
- 長さのプレフィックスを持つ署名付きデータ:
- 長さのプレフィックスが付いたダイジェストのシーケンス (長さのプレフィックス付き):
- 署名アルゴリズム ID (uint32)
- (長さのプレフィックス付き) ダイジェスト - 整合性が保護されたコンテンツを参照
- 長さのプレフィックスを持つ X.509 証明書のシーケンス:
- 長さプレフィックス付きの X.509 証明書 (ASN.1 DER 形式)
- 長さのプレフィックスを含む追加属性 (長さのプレフィックス付き) のシーケンス:
- ID (uint32)
- value (可変長: 追加属性の長さ - 4 バイト)
- 長さのプレフィックスが付いたダイジェストのシーケンス (長さのプレフィックス付き):
- 長さプレフィックス付き署名 (長さプレフィックス付き) シーケンス:
-署名アルゴリズム ID (uint32)
-署名データ長プレフィックス付き署名 - 長さのプレフィックスが付いた公開キー (SubjectPublicKeyInfo、ASN.1 DER 形式)
- 長さのプレフィックスを持つ署名付きデータ:
上記は公式 Web サイトでの V2 署名の紹介であり、以下では上記の紹介に従ってソース コードに従い、具体的には署名ブロックの検索と署名の検証プロセスを見ていきます。
署名ブロックの検索
apk ファイルは実際には zip ファイル圧縮パッケージであり、署名を見つける手順も上で紹介されています。コードの実装は、platform\frameworks\base\core\java\android\util\apk ディレクトリにある ApkSignatureSchemeV2Verifier クラスの findSignature(RandomAccessFile apk) にあります。
public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
パラメータapkは検索対象のapkファイルのRandomAccessFileオブジェクト、APK_SIGNATURE_SCHEME_V2_BLOCK_IDは前述の「APK署名ブロック」に格納されている「APK署名スキームv2ブロック」で、値は0x7109871aです。まず APK 署名ブロックを見つけてから、APK_SIGNATURE_SCHEME_V2_BLOCK_ID を通じて APK 署名スキーム v2 ブロックを見つけます。次に、特定の値を取得します。
ApkSigningBlockUtils.findSignature() メソッドをもう一度見てください。
static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
throws IOException, SignatureNotFoundException {
// Find the ZIP End of Central Directory (EoCD) record.
Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
ByteBuffer eocd = eocdAndOffsetInFile.first;
long eocdOffset = eocdAndOffsetInFile.second;
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
throw new SignatureNotFoundException("ZIP64 APK not supported");
}
// Find the APK Signing Block. The block immediately precedes the Central Directory.
long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
findApkSigningBlock(apk, centralDirOffset);
ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;
// Find the APK Signature Scheme Block inside the APK Signing Block.
ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock,
blockId);
return new SignatureInfo(
apkSignatureSchemeBlock,
apkSigningBlockOffset,
centralDirOffset,
eocdOffset,
eocd);
}
このメソッドは主に次のことを行います。
1. 中央ディレクトリの末尾にあるデータと、apk ファイル内のオフセットを見つけます。
2. 中央ディレクトリを通じて、APK ファイル内の署名ブロック データとそのオフセットを取得します。
3. blockId を通じて v2 ブロック データを検索します。
最後に、関連データが SignatureInfo クラスにカプセル化され、結果が返されます。
中央ディレクトリの末尾にあるデータとapkファイル内のオフセットを見つけます。
これは getEocd(RandomAccessFile apk) によって実装され、getEocd() は ZipUtils クラスの静的メソッド findZipEndOfCentralDirectoryRecord() によって実装されます。これを見てください。
static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)
throws IOException {
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
// The record can be identified by its 4-byte signature/magic which is located at the very
// beginning of the record. A complication is that the record is variable-length because of
// the comment field.
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
// the candidate record's comment length is such that the remainder of the record takes up
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
long fileSize = zip.length();
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
return null;
}
// Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
// the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
// reading more data.
Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
if (result != null) {
return result;
}
// EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
// field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
// the comment length field is an unsigned 16-bit number.
return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
}
主に findZipEndOfCentralDirectoryRecord(RandomAccessFile zip, int maxCommentSize) メソッドによって実装されていることがわかります。2 回呼び出される可能性があることがわかりました。最初の 2 番目のパラメータは 0 です。結果が null の場合、2 番目のパラメータは次のように設定されますUINT16_MAX_VALUE を使用してメソッドを続行します。
クエリがどのように行われるかを理解したい場合は、中央ディレクトリの末尾にあるデータ構造も理解する必要があります。
名前 | 説明する |
---|---|
中央カタログ末尾 | 4バイト固定値(0x06054b50) |
ディスク番号 | 2バイト |
中央ディレクトリが開始されるディスク番号 | 2バイト |
このディスク上の中央ディレクトリ内のエンティティの総数 | 2バイト |
中央ディレクトリ内のエンティティの総数 | 2バイト |
中央ディレクトリのサイズ | 4バイト |
開始ディスク番号からの中央ディレクトリの開始位置のオフセット | 4バイト |
ZIPファイルのコメント長 | 2バイト |
ZIPファイルのコメント | 可変長データ |
zip 圧縮ファイルの最後の部分は中央ディレクトリの末尾で、22 バイトの固定長値と不定長のコメント値で構成されており、この不定長の長さは最大 2 バイトです。apk ファイルの約 99.99% は長さ 0 のコメント値であるため、先頭で中央ディレクトリの終わりを見つけるために 0 を直接使用するのが最適化戦略です。APK の末尾の前の 22 バイトからデータを読み取ります。最初の固定値が 0x06054b50 で、対応するコメントの長さが 0 の場合、中央ディレクトリの末尾が見つかったとみなされます。
長さが 0 で中央ディレクトリの末尾が見つからない場合は、コメントデータがあるとみなされ、最も長い長さの UINT16_MAX_VALUE (65536) がクエリに使用されます。コメントの最初の22バイトは固定長データなので、APK末尾のUINT16_MAX_VALUE + 22バイトのデータを読み込み、最後の22バイトから確認し、最初の固定値0x06054b50が見つかったら、 start from データで取得したコメントの長さと実際のカウントで取得したコメントの長さが一致しており、中央ディレクトリの末尾が見つかったとみなします。
全体的なクエリのアイデアは、上で説明したロジックです。
ここでは、findZipEndOfCentralDirectoryRecord() によって返された結果の Reply<ByteBuffer, Long> についても説明したいと思います。Pair オブジェクトの 2 番目の部分は、APK ファイル内の中央ディレクトリの末尾の位置オフセットです。1 つ目は HeapByteBuffer オブジェクトで、ヒープ上に割り当てられます。上記で、中央ディレクトリの末尾で読み取られるデータを検索し、コメントがある場合は、UINT16_MAX_VALUE + 22 バイトのデータを読み取ることがわかります。ただし、アノテーションの長さは必ずしも UINT16_MAX_VALUE である必要はなく、それより短い場合もあります。では、中央ディレクトリの末尾から HeapByteBuffer オブジェクト内のデータの開始位置をどのように識別するのでしょうか? ByteBuffer には、開始位置を識別するために使用されるメンバー変数 offset があります。
中央ディレクトリを通じて、APK ファイル内の署名ブロック データとオフセットを取得します。
中央ディレクトリの最後にあるデータを取得すると、中央ディレクトリの場所とサイズを取得できます。また、中央ディレクトリの末尾にあるデータ構造の内容も確認してください。ここでは、開始ディスク番号と比較した中央ディレクトリの開始位置のオフセットと、中央ディレクトリのサイズ サブテーブルによって、中央ディレクトリ。
getCentralDirOffset(eocd, eocdOffset) は、これら 2 つのフィールドの値を通じて中央ディレクトリのオフセットを取得します。
次に、findApkSigningBlock(apk,centralDirOffset) は、中央ディレクトリの場所を通じて署名ブロックのデータとオフセットを取得します。
署名ブロックのデータは中央ディレクトリより前にあることがわかり、ブロックのデータ形式から、中央ディレクトリより前の 24 バイトでの署名ブロックのデータ サイズがわかります。このようにして、署名ブロックのオフセットを見つけることができます。findApkSigningBlock(apk,centralDirOffset) は、このロジックに従って見つかります。
v2 ブロックのデータを blockId で検索します
v2 ブロックのデータは、findApkSignatureSchemeBlock(apkSigningBlock, blockId) によって見つかります。ここで、blockId は 0x7109871a とも言われます。
static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId)
throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes pairs
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException(
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount
+ " size out of range: " + lenLong);
}
int len = (int) lenLong;
int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount + " size out of range: " + len
+ ", available: " + pairs.remaining());
}
int id = pairs.getInt();
if (id == blockId) {
return getByteBuffer(pairs, len - 4);
}
pairs.position(nextEntryPos);
}
throw new SignatureNotFoundException(
"No block with ID " + blockId + " in APK Signing Block.");
}
署名ブロックの 8 バイト目は特定の実体データであり、ブロック データの最後から 24 バイト目で終了するためです。したがって、sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24) を通じて ByteBuffer パラメーターを調整することから始めます。具体的な実装を見てみましょう。
static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
int originalLimit = source.limit();
int originalPosition = source.position();
try {
source.position(0);
source.limit(end);
source.position(start);
ByteBuffer result = source.slice();
result.order(source.order());
return result;
} finally {
source.position(0);
source.limit(originalLimit);
source.position(originalPosition);
}
}
実際、新しい ByteBuffer オブジェクトは、ByteBuffer オブジェクトのスライス() メソッドを通じて再生成され、その関連パラメーターが調整されます。ただし、新しく生成された ByteBuffer オブジェクトと元の ByteBuffer オブジェクトは同じ配列を使用します。
ここでは、まずソースの制限をブロック データの最後から 24 バイト目の位置に設定し、ブロックの 8 バイト目の位置に配置してから、slice() メソッドを実行します。この ByteBuffer オブジェクトは実際には HeapByteBuffer オブジェクトであるため、 callはHeapByteBufferオブジェクトのslice()メソッドです。
public ByteBuffer slice() {
return new HeapByteBuffer(hb,
-1,
0,
remaining(),
remaining(),
position() + offset,
isReadOnly);
}
Remaining() は limit-position であるため、新しく生成された ByteBuffer オブジェクトのマークは -1、位置は 0、limit は Remaining()、Capacity は Remaining()、オフセットは Position() + offset、position() は現在、ブロックの 8 バイト目の位置で、オフセットは 0 です。
ByteBuffer オブジェクトのオフセットの設定とは、後でデータを読み込むために、現在位置からオフセット値後の位置に対応するデータを追加することです。
findApkSignatureSchemeBlock() が ByteBuffer オブジェクトを調整したら、v2 ブロック データを検索します。署名ブロック データには、v2 ブロック データだけでなく、他のエンティティ データも含まれる場合があります。これらの各エンティティは、エンティティの長さで始まります。このようにして、この長さを通じて次のエンティティをすぐに見つけることができます。
したがって、findApkSignatureSchemeBlock() はループを使用して、最初にエンティティの長さを取得します。この長さの次のデータは ID 値であるため、エンティティをループして ID 値を比較することで、v2 ブロック データを見つけることができます。v2 ブロック データは getByteBuffer(pairs, len - 4) を通じて取得されます。また、HeapByteBuffer オブジェクトのスライス() メソッドを通じて新しい ByteBuffer オブジェクトも取得されます。
static ByteBuffer getByteBuffer(ByteBuffer source, int size)
throws BufferUnderflowException {
if (size < 0) {
throw new IllegalArgumentException("size: " + size);
}
int originalLimit = source.limit();
int position = source.position();
int limit = position + size;
if ((limit < position) || (limit > originalLimit)) {
throw new BufferUnderflowException();
}
source.limit(limit);
try {
ByteBuffer result = source.slice();
result.order(source.order());
source.position(limit);
return result;
} finally {
source.limit(originalLimit);
}
}
ここで新しく生成された ByteBuffer オブジェクトの利用可能なデータ サイズがパラメータ サイズであることがわかります。そして、slice() は、位置とオフセットの合計に従って ByteBuffer オブジェクトのオフセットをリセットします。新しいオブジェクトを生成した後、元の ByteBuffer オブジェクトのメンバー変数の位置を limit に設定することに注意してください。これは、元の ByteBuffer オブジェクトが新しく生成された ByteBuffer オブジェクトのデータをスキップし、元の ByteBuffer オブジェクトの制限を復元することを示します。
このようにして、ApkSignatureSchemeV2Verifier クラスの findSignature(RandomAccessFile apk) を通じて SignatureInfo オブジェクトが取得されます。これには、署名 v2 ブロック データ、署名ブロックのオフセット位置、中央ディレクトリのオフセット位置、末尾のオフセット位置が含まれます中央ディレクトリのデータと中央ディレクトリ末尾データ。
署名検証プロセス
検証プロセスは ApkSignatureSchemeV2Verifier クラスの verify() に実装されており、コードは次のとおりです。
private static VerifiedSigner verify(
RandomAccessFile apk,
SignatureInfo signatureInfo,
boolean doVerifyIntegrity) throws SecurityException, IOException {
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
List<X509Certificate[]> signerCerts = new ArrayList<>();
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
}
ByteBuffer signers;
try {
signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
} catch (IOException e) {
throw new SecurityException("Failed to read list of signers", e);
}
while (signers.hasRemaining()) {
signerCount++;
try {
ByteBuffer signer = getLengthPrefixedSlice(signers);
X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
signerCerts.add(certs);
} catch (IOException | BufferUnderflowException | SecurityException e) {
throw new SecurityException(
"Failed to parse/verify signer #" + signerCount + " block",
e);
}
}
if (signerCount < 1) {
throw new SecurityException("No signers found");
}
if (contentDigests.isEmpty()) {
throw new SecurityException("No content digests found");
}
if (doVerifyIntegrity) {
ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
}
byte[] verityRootHash = null;
if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
verityDigest, apk.length(), signatureInfo);
}
return new VerifiedSigner(
signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
verityRootHash, contentDigests);
}
V2 ブロック データの形式については、上記のv2 ブロック フォーマットを参照してください。前述したように、signatureInfo.signatureBlock が v2 ブロック データです。最初は int の長さで、後続のすべての署名者の長さになります。getLengthPrefixedSlice(signatureInfo.signatureBlock) は、最初の int 長を読み取ることにより、新しい ByteBuffer オブジェクトの署名者を取得します。
次に、すべての署名者を読み取り、処理するループを開始します。署名者は署名情報です。ここでわかるように、APK では複数の署名を許可できます。
verity は主に次のことを行います。
1. verifySigner(signer, contentDigests, certFactory) を呼び出して署名を検証します。
2. doVerifyIntegrity パラメーターに従って、ファイルの整合性検証を実行するかどうかを決定し、ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, SignatureInfo) を呼び出します。方法、詳細は次の記事Android APK ファイルの整合性検証
3. v2 ブロックに CONTENT_DIGEST_VERITY_CHUNKED_SHA256 アルゴリズムが含まれている場合は、ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength() を呼び出してマークル ツリー ルート ダイジェストを取得します。
最後に、VerifiedSigner オブジェクトにカプセル化されて返されます。これには、各署名者の証明書、マークル ルート ダイジェスト、および各署名者に最適なダイジェスト アルゴリズムが含まれています。
主に署名を検証する最初のステップを見てみましょう
署名を検証する
verifySigner(signer, contentDigests, certFactory) メソッドは少し長いので、最初の段落のセクションに分けて読んでください。
private static X509Certificate[] verifySigner(
ByteBuffer signerBlock,
Map<Integer, byte[]> contentDigests,
CertificateFactory certFactory) throws SecurityException, IOException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
int signatureCount = 0;
int bestSigAlgorithm = -1;
byte[] bestSigAlgorithmSignatureBytes = null;
List<Integer> signaturesSigAlgorithms = new ArrayList<>();
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = getLengthPrefixedSlice(signatures);
if (signature.remaining() < 8) {
throw new SecurityException("Signature record too short");
}
int sigAlgorithm = signature.getInt();
signaturesSigAlgorithms.add(sigAlgorithm);
if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
continue;
}
if ((bestSigAlgorithm == -1)
|| (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
bestSigAlgorithm = sigAlgorithm;
bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
}
} catch (IOException | BufferUnderflowException e) {
throw new SecurityException(
"Failed to parse signature record #" + signatureCount,
e);
}
}
if (bestSigAlgorithm == -1) {
if (signatureCount == 0) {
throw new SecurityException("No signatures found");
} else {
throw new SecurityException("No supported signatures found");
}
}
getLengthPrefixedSlice() を呼び出して、署名データと署名データをそれぞれ取得します。readLengthPrefixedByteArray() を通じて公開鍵データを取得します。
まずループを通過して署名データを処理します。解析するとき、アルゴリズムはsignaturesSigAlgorithmsに入れられます。署名アルゴリズムが複数ある場合は、比較により最良の署名アルゴリズムが選択され、署名データが署名アルゴリズムを通過した後の署名bestSigAlgorithmSignatureBytesが取得されます。
サポートされているアルゴリズムを見てください。
static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
case SIGNATURE_RSA_PSS_WITH_SHA512:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
case SIGNATURE_DSA_WITH_SHA256:
case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
case SIGNATURE_VERITY_DSA_WITH_SHA256:
return true;
default:
return false;
}
}
最適な署名アルゴリズムが見つからない場合は、SecurityException が報告されます。
verifySigner() の 2 番目のコード:
String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
boolean sigVerified;
try {
PublicKey publicKey =
KeyFactory.getInstance(keyAlgorithm)
.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e) {
throw new SecurityException(
"Failed to verify " + jcaSignatureAlgorithm + " signature", e);
}
if (!sigVerified) {
throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
}
まず、最適なアルゴリズムに対応する JCA アルゴリズムを取得し、次に、最適なアルゴリズムに対応する JCA 署名アルゴリズムとパラメータを取得します。次に、公開キーを取得し、公開キーを介して署名されたデータと署名 bestSigAlgorithmSignatureBytes を検証します。検証が失敗した場合は、SecurityException が報告されます。
verifySigner() の 3 番目のコード:
byte[] contentDigest = null;
signedData.clear();
ByteBuffer digests = getLengthPrefixedSlice(signedData);
List<Integer> digestsSigAlgorithms = new ArrayList<>();
int digestCount = 0;
while (digests.hasRemaining()) {
digestCount++;
try {
ByteBuffer digest = getLengthPrefixedSlice(digests);
if (digest.remaining() < 8) {
throw new IOException("Record too short");
}
int sigAlgorithm = digest.getInt();
digestsSigAlgorithms.add(sigAlgorithm);
if (sigAlgorithm == bestSigAlgorithm) {
contentDigest = readLengthPrefixedByteArray(digest);
}
} catch (IOException | BufferUnderflowException e) {
throw new IOException("Failed to parse digest record #" + digestCount, e);
}
}
if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
throw new SecurityException(
"Signature algorithms don't match between digests and signatures records");
}
int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
if ((previousSignerDigest != null)
&& (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
throw new SecurityException(
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ " contents digest does not match the digest specified by a preceding signer");
}
このブロックは signedData 内のダイジェスト シーケンスを処理し、そのデータ構造はv2 ブロック形式を参照します。要約アルゴリズムを取得し、その要約アルゴリズムを DigestsSigAlgorithms に入れます。そして、以前の最適なアルゴリズム bestSigAlgorithm と同じである場合、要約データは contentDigest に読み込まれます。
読み取り後、digestsSigAlgorithms と SignatureSigAlgorithms が比較されます。異なる場合は、例外が報告されます。
次に、 getSignatureAlgorithmContentDigestAlgorithm() メソッドを使用して、署名アルゴリズムを対応するダイジェスト アルゴリズムに変換します。また、ダイジェスト アルゴリズムとダイジェスト データをパラメータ contentDigests に入力します。ダイジェスト アルゴリズムが前の署名で使用されており、前のダイジェストが現在のダイジェストと異なる場合、SecurityException がスローされます。
verifySigner() の最後のコード部分:
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
List<X509Certificate> certs = new ArrayList<>();
int certificateCount = 0;
while (certificates.hasRemaining()) {
certificateCount++;
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = (X509Certificate)
certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
}
certificate = new VerbatimX509Certificate(certificate, encodedCert);
certs.add(certificate);
}
if (certs.isEmpty()) {
throw new SecurityException("No certificates listed");
}
X509Certificate mainCertificate = certs.get(0);
byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
throw new SecurityException(
"Public key mismatch between certificate and signature record");
}
ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
verifyAdditionalAttributes(additionalAttrs);
return certs.toArray(new X509Certificate[certs.size()]);
}
このとき、signedData 内の X.509 証明書シーケンスを処理するために、データ構造はv2 ブロック形式を参照します。整数データは VerbatimX509Certificate オブジェクトにカプセル化され、証明書に入れられます。証明書がない場合は、SecurityException が報告されます。
また、certs の最初の証明書の公開鍵が、先ほど読み出した公開鍵データ publicKeyBytes と異なる場合も、SecurityException が報告されます。
次に、signedData 内の追加属性シーケンスが処理され、データの構造はv2 ブロック形式を参照します。verifyAdditionalAttributes(ByteBuffer attrs) を呼び出して処理します。
返される最終結果は証明書の配列です。
要約する
この記事では主に、公式ドキュメントに基づいて、ソース コードと組み合わせて、V2 署名の検索と検証のプロセスを再構成します。
この処理では、HeapByteBuffer クラスを使用して読み込んだデータを格納するため、HeapByteBuffer クラスの使い方、特にslice()の使い方を理解する必要があります。
V2 署名ブロックを探すときは、APK ファイルの構造を理解し、署名データ ブロックが中央ディレクトリの前に保存されていることを理解し、署名ブロックのデータ構造を知る必要があります。
署名検証プロセスでは、v2 ブロックのデータ ストレージ構造を理解しておく必要があります。署名が必要なデータ、署名データ、署名アルゴリズムと公開キーを見つけることによって。その後、公開キーを使用して、秘密キーで署名されたデータを検証できます。