blob: 866402f9808f40e628a419e3802461fe49aef9c8 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.asn1.Asn1EncodingException;
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* APK Signature Scheme V4 signer.
* V4 scheme file contains 3 mandatory fields - used during installation.
* And optional verity tree - has to be present during session commit.
*
* The fields:
* <p>
* 1. verityRootHash: bytes of the hash tree root (digest of first 1-page of the tree),
* 2. V3Digest: digest from v2/v3 signing schema,
* 3. pkcs7SignatureBlock: bytes of the signature over encoded signed data, which includes 1 and 2,
* </p>
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
*
* TODO(schfan): Pass v3 digest to v4 signature proto and add verification code
* TODO(schfan): Add v4 unit tests
*/
public abstract class V4SchemeSigner {
/** Hidden constructor to prevent instantiation. */
private V4SchemeSigner() {
}
/**
* Based on a public key, return a signing algorithm that supports verity.
*/
public static SignatureAlgorithm getSuggestedSignatureAlgorithm(PublicKey signingKey)
throws InvalidKeyException {
final String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
final int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
if (modulusLengthBits <= 3072) {
return SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
} else {
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
throw new InvalidKeyException(
"Key requires SHA-512 signature algorithm, not yet supported with verity");
}
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
return SignatureAlgorithm.VERITY_DSA_WITH_SHA256;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
final int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
if (keySizeBits <= 256) {
return SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256;
} else {
throw new InvalidKeyException(
"Key requires SHA-512 signature algorithm, not yet supported with verity");
}
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
/**
* Compute hash tree and root for a given APK. Write the serialized date to output file.
*/
public static void generateV4Signature(
DataSource apkContent,
SignerConfig signerConfig,
File outputFile)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> verityDigest = new HashMap<>();
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent, verityDigest);
byte[] v3digest;
try {
v3digest = getV3Digest(apkContent);
} catch (ApkFormatException | SignatureException
| ApkSigningBlockUtils.SignatureNotFoundException e) {
throw new IOException("Failed to parse V3-signed apk to read its V3 digest");
}
final Pair<V4Signature, byte[]> signaturePair;
try {
signaturePair = generateSignatureObject(signerConfig, verityDigest, v3digest);
} catch (InvalidKeyException | SignatureException |
CertificateEncodingException | Asn1EncodingException e) {
throw new InvalidKeyException("Signer failed", e);
}
V4Signature signature = signaturePair.getFirst();
byte[] tree = signaturePair.getSecond();
try (final DataOutputStream output = new DataOutputStream(
new FileOutputStream(outputFile))) {
signature.writeTo(output);
if (tree != null && tree.length != 0) {
V4Signature.writeBytes(output, tree);
}
}
}
private static Pair<V4Signature, byte[]> generateSignatureObject(
SignerConfig signerConfig,
Map<ContentDigestAlgorithm, Pair<byte[], byte[]>> contentDigests,
byte[] v3Digest)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
CertificateEncodingException, Asn1EncodingException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
final List<byte[]> certificates = encodeCertificates(signerConfig.certificates);
if (certificates.size() != 1) {
throw new CertificateEncodingException("Should only have one certificate");
}
if (signerConfig.signatureAlgorithms.size() != 1) {
throw new SignatureException("Should only be one signature algorithm");
}
final SignatureAlgorithm signatureAlgorithm = signerConfig.signatureAlgorithms.get(0);
final ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
final Pair<byte[], byte[]> contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new SignatureException("Cannot find computed digest");
}
byte[] tree = contentDigest.getFirst();
byte[] rootHash = contentDigest.getSecond();
byte[] data = ByteBuffer.allocate(rootHash.length + v3Digest.length)
.put(rootHash).put(v3Digest).array();
final List<Pair<Integer, byte[]>> signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
if (signatures.size() != 1) {
throw new SignatureException("Should only be one signature generated");
}
byte[] pkcs7SignatureBlock = ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
signatures.get(0).getSecond(), /* signature bytes */
ByteBuffer.wrap(data),
signerConfig.certificates,
new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL), /* digest algo id */
getSignatureAlgorithmIdentifier(publicKey));
final V4Signature signature =
new V4Signature(V4Signature.CURRENT_VERSION,
rootHash, v3Digest, pkcs7SignatureBlock);
return Pair.of(signature, tree);
}
// Get V3 digest by parsing the V3-signed apk
private static byte[] getV3Digest(DataSource apk) throws ApkFormatException, IOException,
ApkSigningBlockUtils.SignatureNotFoundException, NoSuchAlgorithmException,
SignatureException {
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
ApkUtils.ZipSections zipSections;
try {
zipSections = ApkUtils.findZipSections(apk);
} catch (ZipFormatException e) {
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
}
final SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
if (result.signers.size() != 1) {
throw new SignatureException("Should only have one signer");
}
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
result.signers.get(0).contentDigests;
if (contentDigests.isEmpty()) {
throw new SignatureException("Should have at least one digest");
}
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
final SignatureAlgorithm signatureAlgorithm =
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
if (signatureAlgorithm == null) {
continue;
}
final ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
if (contentDigestAlgorithm == null) {
continue;
}
if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
|| contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
return contentDigest.getValue();
}
}
throw new SignatureException("Failed to find any V3 digest in the source APK");
}
/**
* Returns the JCA SHA256 {@code AlgorithmIdentifier} and PKCS #7 {@code SignatureAlgorithm} to
* use when signing with the specified key.
*/
private static AlgorithmIdentifier getSignatureAlgorithmIdentifier(
PublicKey publicKey) throws InvalidKeyException {
String keyAlgorithm = publicKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
return new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL);
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
return new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL);
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
}