From 85b8a0146ec14cd1a835eec4831765a4b836def0 Mon Sep 17 00:00:00 2001 From: Alex Klyubin Date: Thu, 5 May 2016 10:50:24 -0700 Subject: [PATCH] JAR signing logic for the apksigner-core library. apksigner-code library will offer a high-level primitive (future commit) for signing APKs. This is meant to be used by build/tools/signapk and Android Studio's APK builder/signer. This commit adds a lower-level JAR signing (aka v1 signing) code which will be used by the future APK signing abstraction exposed by this library. All classes added by this commit are internal (i.e., implementation details of this library). Clients of this library should not be using these classes. Bug: 26516150 Change-Id: I5cecd435e63aab206d63868be5e0d0e289e7c423 --- tools/apksigner/Android.mk | 19 + tools/apksigner/core/Android.mk | 26 + .../core/internal/apk/v1/DigestAlgorithm.java | 42 ++ .../core/internal/apk/v1/V1SchemeSigner.java | 526 ++++++++++++++++++ .../core/internal/jar/ManifestWriter.java | 124 +++++ .../internal/jar/SignatureFileWriter.java | 59 ++ .../apksigner/core/internal/util/Pair.java | 81 +++ 7 files changed, 877 insertions(+) create mode 100644 tools/apksigner/Android.mk create mode 100644 tools/apksigner/core/Android.mk create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java diff --git a/tools/apksigner/Android.mk b/tools/apksigner/Android.mk new file mode 100644 index 0000000000..a7b4414c5f --- /dev/null +++ b/tools/apksigner/Android.mk @@ -0,0 +1,19 @@ +# +# Copyright (C) 2016 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. +# + +LOCAL_PATH := $(call my-dir) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/apksigner/core/Android.mk b/tools/apksigner/core/Android.mk new file mode 100644 index 0000000000..c86208be97 --- /dev/null +++ b/tools/apksigner/core/Android.mk @@ -0,0 +1,26 @@ +# +# Copyright (C) 2016 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. +# +LOCAL_PATH := $(call my-dir) + +# apksigner library, for signing APKs and verification signatures of APKs +# ============================================================ +include $(CLEAR_VARS) +LOCAL_MODULE := apksigner-core +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_JAVA_LIBRARIES = \ + bouncycastle-host \ + bouncycastle-bcpkix-host +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java new file mode 100644 index 0000000000..71e698b99b --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 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.apksigner.core.internal.apk.v1; + +/** + * Digest algorithm used with JAR signing (aka v1 signing scheme). + */ +public enum DigestAlgorithm { + /** SHA-1 */ + SHA1("SHA-1"), + + /** SHA2-256 */ + SHA256("SHA-256"); + + private final String mJcaMessageDigestAlgorithm; + + private DigestAlgorithm(String jcaMessageDigestAlgoritm) { + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm; + } + + /** + * Returns the {@link java.security.MessageDigest} algorithm represented by this digest + * algorithm. + */ + String getJcaMessageDigestAlgorithm() { + return mJcaMessageDigestAlgorithm; + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java new file mode 100644 index 0000000000..b99cdeccc1 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2016 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.apksigner.core.internal.apk.v1; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.DEROutputStream; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +import com.android.apksigner.core.internal.jar.ManifestWriter; +import com.android.apksigner.core.internal.jar.SignatureFileWriter; +import com.android.apksigner.core.internal.util.Pair; + +/** + * APK signer which uses JAR signing (aka v1 signing scheme). + * + * @see Signed JAR File + */ +public abstract class V1SchemeSigner { + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + + private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = + new Attributes.Name("Created-By"); + private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)"; + private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; + private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; + + private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = + new Attributes.Name("X-Android-APK-Signed"); + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Name. */ + public String name; + + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List certificates; + + /** + * Digest algorithm used for the signature. + */ + public DigestAlgorithm signatureDigestAlgorithm; + + /** + * Digest algorithm used for digests of JAR entries and MANIFEST.MF. + */ + public DigestAlgorithm contentDigestAlgorithm; + } + + /** Hidden constructor to prevent instantiation. */ + private V1SchemeSigner() {} + + /** + * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + * + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using + * JAR signing (aka v1 signature scheme) + */ + public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( + PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 18, only SHA-1 can be used with RSA. + if (minSdkVersion < 18) { + return DigestAlgorithm.SHA1; + } + return DigestAlgorithm.SHA256; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 21, only SHA-1 can be used with DSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + if (minSdkVersion < 18) { + throw new InvalidKeyException( + "ECDSA signatures only supported for minSdkVersion 18 and higher"); + } + // Prior to API Level 21, only SHA-1 can be used with ECDSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + /** + * Returns the JAR signing digest algorithm to be used for JAR entry digests. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + */ + public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) { + return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1; + } + + /** + * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm. + */ + public static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) { + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + try { + return MessageDigest.getInstance(jcaAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to obtain " + jcaAlgorithm + " MessageDigest", e); + } + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest. + */ + public static boolean isJarEntryDigestNeededInManifest(String entryName) { + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File + + // Entries outside of META-INF must be listed in the manifest. + if (!entryName.startsWith("META-INF/")) { + return true; + } + // Entries in subdirectories of META-INF must be listed in the manifest. + if (entryName.indexOf('/', "META-INF/".length()) != -1) { + return true; + } + + // Ignored file names (case-insensitive) in META-INF directory: + // MANIFEST.MF + // *.SF + // *.RSA + // *.DSA + // *.EC + // SIG-* + String fileNameLowerCase = + entryName.substring("META-INF/".length()).toLowerCase(Locale.US); + if (("manifest.mf".equals(fileNameLowerCase)) + || (fileNameLowerCase.endsWith(".sf")) + || (fileNameLowerCase.endsWith(".rsa")) + || (fileNameLowerCase.endsWith(".dsa")) + || (fileNameLowerCase.endsWith(".ec")) + || (fileNameLowerCase.startsWith("sig-"))) { + return false; + } + return true; + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List> sign( + List signerConfigs, + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + List apkSigningSchemeIds, + byte[] sourceManifestBytes) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + OutputManifestFile manifest = + generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes); + + return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest); + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List> signManifest( + List signerConfigs, + DigestAlgorithm digestAlgorithm, + List apkSigningSchemeIds, + OutputManifestFile manifest) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + + // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF. + List> signatureJarEntries = + new ArrayList<>(2 * signerConfigs.size() + 1); + byte[] sfBytes = + generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + byte[] signatureBlock; + try { + signatureBlock = generateSignatureBlock(signerConfig, sfBytes); + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (CertificateEncodingException e) { + throw new CertificateEncodingException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (SignatureException e) { + throw new SignatureException( + "Failed to sign using signer \"" + signerName + "\"", e); + } + signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes)); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + signatureJarEntries.add( + Pair.of(signatureBlockFileName, signatureBlock)); + } + signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents)); + return signatureJarEntries; + } + + /** + * Returns the names of JAR entries which this signer will produce as part of v1 signature. + */ + public static Set getOutputEntryNames(List signerConfigs) { + Set result = new HashSet<>(2 * signerConfigs.size() + 1); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + result.add("META-INF/" + signerName + ".SF"); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + result.add(signatureBlockFileName); + } + result.add(MANIFEST_ENTRY_NAME); + return result; + } + + /** + * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional) + * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest. + */ + public static OutputManifestFile generateManifestFile( + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + byte[] sourceManifestBytes) { + Manifest sourceManifest = null; + if (sourceManifestBytes != null) { + try { + sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes)); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e); + } + } + ByteArrayOutputStream manifestOut = new ByteArrayOutputStream(); + Attributes mainAttrs = new Attributes(); + // Copy the main section from the source manifest (if provided). Otherwise use defaults. + if (sourceManifest != null) { + mainAttrs.putAll(sourceManifest.getMainAttributes()); + } else { + mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); + } + + try { + ManifestWriter.writeMainSection(manifestOut, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + + List sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); + Collections.sort(sortedEntryNames); + SortedMap invidualSectionsContents = new TreeMap<>(); + String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm); + for (String entryName : sortedEntryNames) { + byte[] entryDigest = jarEntryDigests.get(entryName); + Attributes entryAttrs = new Attributes(); + entryAttrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(entryDigest)); + ByteArrayOutputStream sectionOut = new ByteArrayOutputStream(); + byte[] sectionBytes; + try { + ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs); + sectionBytes = sectionOut.toByteArray(); + manifestOut.write(sectionBytes); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + invidualSectionsContents.put(entryName, sectionBytes); + } + + OutputManifestFile result = new OutputManifestFile(); + result.contents = manifestOut.toByteArray(); + result.mainSectionAttributes = mainAttrs; + result.individualSectionsContents = invidualSectionsContents; + return result; + } + + public static class OutputManifestFile { + public byte[] contents; + public SortedMap individualSectionsContents; + public Attributes mainSectionAttributes; + } + + private static byte[] generateSignatureFile( + List apkSignatureSchemeIds, + DigestAlgorithm manifestDigestAlgorithm, + OutputManifestFile manifest) { + Manifest sf = new Manifest(); + Attributes mainAttrs = sf.getMainAttributes(); + mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); + if (!apkSignatureSchemeIds.isEmpty()) { + // Add APK Signature Scheme v2 (and newer) signature stripping protection. + // This attribute indicates that this APK is supposed to have been signed using one or + // more APK-specific signature schemes in addition to the standard JAR signature scheme + // used by this code. APK signature verifier should reject the APK if it does not + // contain a signature for the signature scheme the verifier prefers out of this set. + StringBuilder attrValue = new StringBuilder(); + for (int id : apkSignatureSchemeIds) { + if (attrValue.length() > 0) { + attrValue.append(", "); + } + attrValue.append(String.valueOf(id)); + } + mainAttrs.put( + SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME, + attrValue.toString()); + } + + // Add main attribute containing the digest of MANIFEST.MF. + MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm); + mainAttrs.putValue( + getManifestDigestAttributeName(manifestDigestAlgorithm), + Base64.getEncoder().encodeToString(md.digest(manifest.contents))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + SignatureFileWriter.writeMainSection(out, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm); + for (Map.Entry manifestSection + : manifest.individualSectionsContents.entrySet()) { + String sectionName = manifestSection.getKey(); + byte[] sectionContents = manifestSection.getValue(); + byte[] sectionDigest = md.digest(sectionContents); + Attributes attrs = new Attributes(); + attrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(sectionDigest)); + + try { + SignatureFileWriter.writeIndividualSection(out, sectionName, attrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + } + + // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will + // cause a spurious IOException to be thrown if the length of the signature file is a + // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case. + if ((out.size() > 0) && ((out.size() % 1024) == 0)) { + try { + SignatureFileWriter.writeSectionDelimiter(out); + } catch (IOException e) { + throw new RuntimeException("Failed to write to ByteArrayOutputStream", e); + } + } + + return out.toByteArray(); + } + + private static byte[] generateSignatureBlock( + SignerConfig signerConfig, byte[] signatureFileBytes) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + JcaCertStore certs = new JcaCertStore(signerConfig.certificates); + X509Certificate signerCert = signerConfig.certificates.get(0); + String jcaSignatureAlgorithm = + getJcaSignatureAlgorithm( + signerCert.getPublicKey(), signerConfig.signatureDigestAlgorithm); + try { + ContentSigner signer = + new JcaContentSignerBuilder(jcaSignatureAlgorithm) + .build(signerConfig.privateKey); + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + gen.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build()) + .setDirectSignature(true) + .build(signer, signerCert)); + gen.addCertificates(certs); + + CMSSignedData sigData = + gen.generate(new CMSProcessableByteArray(signatureFileBytes), false); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { + DEROutputStream dos = new DEROutputStream(out); + dos.writeObject(asn1.readObject()); + } + return out.toByteArray(); + } catch (OperatorCreationException | CMSException | IOException e) { + throw new SignatureException("Failed to generate signature", e); + } + } + + private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest"; + case SHA256: + return "SHA-256-Digest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest-Manifest"; + case SHA256: + return "SHA-256-Digest-Manifest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getJcaSignatureAlgorithm( + PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException { + String keyAlgorithm = publicKey.getAlgorithm(); + String digestPrefixForSigAlg; + switch (digestAlgorithm) { + case SHA1: + digestPrefixForSigAlg = "SHA1"; + break; + case SHA256: + digestPrefixForSigAlg = "SHA256"; + break; + default: + throw new IllegalArgumentException( + "Unexpected digest algorithm: " + digestAlgorithm); + } + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withRSA"; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withDSA"; + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withECDSA"; + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java new file mode 100644 index 0000000000..449953a68b --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 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.apksigner.core.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; + +/** + * Producer of {@code META-INF/MANIFEST.MF} file. + */ +public abstract class ManifestWriter { + + private static final byte[] CRLF = new byte[] {'\r', '\n'}; + private static final int MAX_LINE_LENGTH = 70; + + private ManifestWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Manifest-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION); + if (manifestVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing"); + } + writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion); + + if (attributes.size() > 1) { + SortedMap namedAttributes = getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString()); + writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + writeAttribute(out, "Name", name); + + if (!attributes.isEmpty()) { + writeAttributes(out, getAttributesSortedByName(attributes)); + } + writeSectionDelimiter(out); + } + + static void writeSectionDelimiter(OutputStream out) throws IOException { + out.write(CRLF); + } + + static void writeAttribute(OutputStream out, Attributes.Name name, String value) + throws IOException { + writeAttribute(out, name.toString(), value); + } + + private static void writeAttribute(OutputStream out, String name, String value) + throws IOException { + writeLine(out, name + ": " + value); + } + + private static void writeLine(OutputStream out, String line) throws IOException { + byte[] lineBytes = line.getBytes("UTF-8"); + int offset = 0; + int remaining = lineBytes.length; + boolean firstLine = true; + while (remaining > 0) { + int chunkLength; + if (firstLine) { + // First line + chunkLength = Math.min(remaining, MAX_LINE_LENGTH); + } else { + // Continuation line + out.write(CRLF); + out.write(' '); + chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1); + } + out.write(lineBytes, offset, chunkLength); + offset += chunkLength; + remaining -= chunkLength; + firstLine = false; + } + out.write(CRLF); + } + + static SortedMap getAttributesSortedByName(Attributes attributes) { + Set> attributesEntries = attributes.entrySet(); + SortedMap namedAttributes = new TreeMap(); + for (Map.Entry attribute : attributesEntries) { + String attrName = attribute.getKey().toString(); + String attrValue = attribute.getValue().toString(); + namedAttributes.put(attrName, attrValue); + } + return namedAttributes; + } + + static void writeAttributes( + OutputStream out, SortedMap attributesSortedByName) throws IOException { + for (Map.Entry attribute : attributesSortedByName.entrySet()) { + String attrName = attribute.getKey(); + String attrValue = attribute.getValue(); + writeAttribute(out, attrName, attrValue); + } + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java new file mode 100644 index 0000000000..9cd25f3c35 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 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.apksigner.core.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.SortedMap; +import java.util.jar.Attributes; + +/** + * Producer of JAR signature file ({@code *.SF}). + */ +public abstract class SignatureFileWriter { + private SignatureFileWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Signature-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION); + if (signatureVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing"); + } + ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion); + + if (attributes.size() > 1) { + SortedMap namedAttributes = + ManifestWriter.getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString()); + ManifestWriter.writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + ManifestWriter.writeIndividualSection(out, name, attributes); + } + + public static void writeSectionDelimiter(OutputStream out) throws IOException { + ManifestWriter.writeSectionDelimiter(out); + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java new file mode 100644 index 0000000000..d59af410b3 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 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.apksigner.core.internal.util; + +/** + * Pair of two elements. + */ +public final class Pair { + private final A mFirst; + private final B mSecond; + + private Pair(A first, B second) { + mFirst = first; + mSecond = second; + } + + public static Pair of(A first, B second) { + return new Pair(first, second); + } + + public A getFirst() { + return mFirst; + } + + public B getSecond() { + return mSecond; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); + result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!mFirst.equals(other.mFirst)) { + return false; + } + if (mSecond == null) { + if (other.mSecond != null) { + return false; + } + } else if (!mSecond.equals(other.mSecond)) { + return false; + } + return true; + } +}