• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2023-2023 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package com.ohos.hapsigntool.codesigning.sign;
17 
18 import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlock;
19 import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlockHeader;
20 import com.ohos.hapsigntool.codesigning.datastructure.ElfSignBlock;
21 import com.ohos.hapsigntool.codesigning.datastructure.Extension;
22 import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment;
23 import com.ohos.hapsigntool.codesigning.datastructure.HapInfoSegment;
24 import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension;
25 import com.ohos.hapsigntool.codesigning.datastructure.NativeLibInfoSegment;
26 import com.ohos.hapsigntool.codesigning.datastructure.SegmentHeader;
27 import com.ohos.hapsigntool.codesigning.datastructure.SignInfo;
28 import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException;
29 import com.ohos.hapsigntool.codesigning.exception.PageInfoException;
30 import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException;
31 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator;
32 import com.ohos.hapsigntool.codesigning.utils.CmsUtils;
33 import com.ohos.hapsigntool.codesigning.utils.HapUtils;
34 import com.ohos.hapsigntool.entity.Pair;
35 import com.ohos.hapsigntool.error.ProfileException;
36 import com.ohos.hapsigntool.utils.LogUtils;
37 import com.ohos.hapsigntool.utils.StringUtils;
38 
39 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
40 import org.bouncycastle.asn1.cms.Attribute;
41 import org.bouncycastle.asn1.cms.AttributeTable;
42 import org.bouncycastle.cms.CMSException;
43 import org.bouncycastle.cms.CMSSignedData;
44 import org.bouncycastle.cms.SignerInformation;
45 
46 import java.io.File;
47 import java.io.FileInputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.util.Arrays;
51 import java.util.Collection;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.Locale;
55 import java.util.Map;
56 import java.util.Set;
57 import java.util.jar.JarEntry;
58 import java.util.jar.JarFile;
59 import java.util.zip.ZipInputStream;
60 
61 /**
62  * Verify code signature given a file with code sign block
63  *
64  * @since 2023/09/08
65  */
66 public class VerifyCodeSignature {
67     private static final LogUtils LOGGER = new LogUtils(VerifyCodeSignature.class);
68 
checkOwnerID(byte[] signature, String profileOwnerID, String profileType)69     private static void checkOwnerID(byte[] signature, String profileOwnerID, String profileType)
70         throws CMSException, VerifyCodeSignException {
71         String ownerID = profileOwnerID;
72         // if profileType is debug, check the app-id in signature, should be null or DEBUG_LIB_ID
73         if ("debug".equals(profileType)) {
74             ownerID = HapUtils.HAP_DEBUG_OWNER_ID;
75         }
76         checkSignatureOwnerID(ownerID, signature, profileType);
77     }
78 
checkHnpOwnerID(byte[] signature, String profileOwnerID, String profileType, String hnpType)79     private static void checkHnpOwnerID(byte[] signature, String profileOwnerID, String profileType, String hnpType)
80         throws CMSException, VerifyCodeSignException {
81         String ownerID = profileOwnerID;
82         // if profileType is debug, check the app-id in signature, should be null or DEBUG_LIB_ID
83         if ("debug".equals(profileType)) {
84             ownerID = HapUtils.HAP_DEBUG_OWNER_ID;
85         } else if ("release".equals(profileType)) {
86             if ("public".equals(hnpType)) {
87                 ownerID = HapUtils.HAP_SHARED_OWNER_ID;
88             }
89         }
90         checkSignatureOwnerID(ownerID, signature, profileType);
91     }
92 
checkSignatureOwnerID(String ownerID, byte[] signature, String profileType)93     private static void checkSignatureOwnerID(String ownerID, byte[] signature, String profileType)
94         throws CMSException, VerifyCodeSignException {
95         CMSSignedData cmsSignedData = new CMSSignedData(signature);
96         Collection<SignerInformation> signers = cmsSignedData.getSignerInfos().getSigners();
97         for (SignerInformation signer : signers) {
98             AttributeTable attrTable = signer.getSignedAttributes();
99             Attribute attr = attrTable.get(new ASN1ObjectIdentifier(BcSignedDataGenerator.SIGNER_OID));
100             // if app-id is null, if profileType is debug, it's ok.
101             if (attr == null) {
102                 if ("debug".equals(profileType)) {
103                     continue;
104                 }
105                 if (ownerID == null) {
106                     continue;
107                 } else {
108                     throw new VerifyCodeSignException("app-identifier is not in the signature");
109                 }
110             }
111             if (ownerID == null) {
112                 throw new VerifyCodeSignException("app-identifier in profile is null, but is not null in signature");
113             }
114             // if app-id in signature exists, it should be equal to the app-id in profile.
115             String resultOwnerID = attr.getAttrValues().getObjectAt(0).toString();
116             if (!ownerID.equals(resultOwnerID)) {
117                 throw new VerifyCodeSignException("app-identifier in signature is invalid");
118             }
119         }
120     }
121 
122     /**
123      * Verify a signed elf's signature
124      *
125      * @param file       signed elf file
126      * @param offset     start position of code sign block based on the start of the elf file
127      * @param length     byte size of code sign block
128      * @param fileFormat elf
129      * @param profileContent profile json string
130      * @return true if signature verify succeed and false otherwise
131      * @throws IOException             If an input or output exception occurred
132      * @throws VerifyCodeSignException parsing result invalid
133      * @throws FsVerityDigestException if fs-verity digest generation failed
134      * @throws CMSException            if signature verify failed
135      * @throws ProfileException        if verify profile failed
136      */
verifyElf(File file, long offset, long length, String fileFormat, String profileContent)137     public static boolean verifyElf(File file, long offset, long length, String fileFormat, String profileContent)
138             throws IOException, VerifyCodeSignException, FsVerityDigestException, CMSException, ProfileException {
139         if (!CodeSigning.SUPPORT_BIN_FILE_FORM.equalsIgnoreCase(fileFormat)) {
140             LOGGER.info("Not elf file, skip code signing verify");
141             return true;
142         }
143         // 1) parse sign block to ElfCodeSignBlock object
144         ElfSignBlock elfSignBlock;
145         try (FileInputStream signedElf = new FileInputStream(file)) {
146             byte[] codeSignBlockBytes = new byte[(int) length];
147             signedElf.skip(offset);
148             signedElf.read(codeSignBlockBytes);
149             elfSignBlock = ElfSignBlock.fromByteArray(codeSignBlockBytes);
150         }
151         // 2) verify file data
152         try (FileInputStream signedElf = new FileInputStream(file)) {
153             int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset);
154             byte[] merkleTreeWithPadding = elfSignBlock.getMerkleTreeWithPadding();
155             byte[] merkleTree = Arrays.copyOfRange(merkleTreeWithPadding, paddingSize, merkleTreeWithPadding.length);
156             verifySingleFile(signedElf, elfSignBlock.getDataSize(), elfSignBlock.getSignature(),
157                 elfSignBlock.getTreeOffset(), merkleTree);
158         }
159         if (profileContent != null) {
160             Pair<String, String> pairResult = HapUtils.parseAppIdentifier(profileContent);
161             checkOwnerID(elfSignBlock.getSignature(), pairResult.getFirst(), pairResult.getSecond());
162         }
163         return true;
164     }
165 
166     /**
167      * Verify a signed hap's signature
168      *
169      * @param file       signed hap file
170      * @param offset     start position of code sign block based on the start of the hap file
171      * @param length     byte size of code sign block
172      * @param fileFormat hap or hqf or hsp, etc.
173      * @param profileContent profile of the hap
174      * @return true if signature verify succeed and false otherwise
175      * @throws IOException             If an input or output exception occurred
176      * @throws VerifyCodeSignException parsing result invalid
177      * @throws FsVerityDigestException if fs-verity digest generation failed
178      * @throws CMSException            if signature verify failed
179      * @throws ProfileException        profile of the hap failed
180      */
verifyHap(File file, long offset, long length, String fileFormat, String profileContent)181     public static boolean verifyHap(File file, long offset, long length, String fileFormat, String profileContent)
182             throws IOException, VerifyCodeSignException, FsVerityDigestException, CMSException, ProfileException {
183         if (!StringUtils.containsIgnoreCase(CodeSigning.SUPPORT_FILE_FORM, fileFormat)) {
184             LOGGER.info("Not hap, hsp or hqf file, skip code signing verify");
185             return true;
186         }
187         Pair<String, String> pairResult = HapUtils.parseAppIdentifier(profileContent);
188 
189         CodeSignBlock csb = generateCodeSignBlock(file, offset, length);
190         // 2) verify hap
191         try (FileInputStream hap = new FileInputStream(file)) {
192             long dataSize = csb.getHapInfoSegment().getSignInfo().getDataSize();
193             byte[] signature = csb.getHapInfoSegment().getSignInfo().getSignature();
194             Extension extension = csb.getHapInfoSegment()
195                 .getSignInfo()
196                 .getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED);
197             MerkleTreeExtension mte = new MerkleTreeExtension(0, 0, null);
198             if (extension instanceof MerkleTreeExtension) {
199                 mte = (MerkleTreeExtension) extension;
200             }
201             // temporary: merkle tree offset set to zero, change to merkleTreeOffset
202             verifySingleFile(hap, dataSize, signature, mte.getMerkleTreeOffset(),
203                 csb.getOneMerkleTreeByFileName(CodeSigning.HAP_SIGNATURE_ENTRY_NAME));
204             checkOwnerID(signature, pairResult.getFirst(), pairResult.getSecond());
205         }
206         // 3) verify native libs
207         verifyLibs(file, csb, pairResult);
208         return true;
209     }
210 
verifyLibs(File file, CodeSignBlock csb, Pair<String, String> pairResult)211     private static void verifyLibs(File file, CodeSignBlock csb, Pair<String, String> pairResult)
212         throws IOException, FsVerityDigestException, VerifyCodeSignException, CMSException, ProfileException {
213         try (JarFile inputJar = new JarFile(file, false)) {
214             Map<String, SignInfo> hnpLibSignInfoMap = new HashMap<>();
215             Set<String> hnpEntryNames = new HashSet<>();
216             for (int i = 0; i < csb.getSoInfoSegment().getSectionNum(); i++) {
217                 String entryName = csb.getSoInfoSegment().getFileNameList().get(i);
218                 SignInfo signInfo = csb.getSoInfoSegment().getSignInfoList().get(i);
219                 if (entryName.contains("!/")) {
220                     String[] filePath = entryName.split("!/");
221                     hnpEntryNames.add(filePath[0]);
222                     hnpLibSignInfoMap.put(entryName, signInfo);
223                 } else {
224                     LOGGER.info("verify lib: {}", entryName);
225                     verifyHapLib(inputJar, entryName, signInfo, pairResult);
226                 }
227             }
228             if (hnpEntryNames.isEmpty()) {
229                 // not exists hnp lib
230                 return;
231             }
232             // get module.json
233             Map<String, String> hnpTypeMap = HapUtils.getHnpsFromJson(inputJar);
234             for (String hnpEntryName : hnpEntryNames) {
235                 verifyHnpLib(inputJar, hnpEntryName, hnpLibSignInfoMap, hnpTypeMap, pairResult);
236             }
237         }
238     }
239 
verifyHapLib(JarFile inputJar, String entryName, SignInfo signInfo, Pair<String, String> pairResult)240     private static void verifyHapLib(JarFile inputJar, String entryName, SignInfo signInfo,
241         Pair<String, String> pairResult)
242         throws IOException, FsVerityDigestException, VerifyCodeSignException, CMSException {
243 
244         JarEntry entry = inputJar.getJarEntry(entryName);
245         if (entry.getSize() != signInfo.getDataSize()) {
246             throw new VerifyCodeSignException(
247                 String.format(Locale.ROOT, "Invalid dataSize of native lib %s", entryName));
248         }
249         byte[] entrySig = signInfo.getSignature();
250         try (InputStream entryInputStream = inputJar.getInputStream(entry)) {
251             // temporary merkleTreeOffset 0
252             verifySingleFile(entryInputStream, entry.getSize(), entrySig, 0, null);
253             checkOwnerID(entrySig, pairResult.getFirst(), pairResult.getSecond());
254         }
255     }
256 
verifyHnpLib(JarFile inputJar, String hnpEntryName, Map<String, SignInfo> hnpLibSignInfoMap, Map<String, String> hnpTypeMap, Pair<String, String> pairResult)257     private static void verifyHnpLib(JarFile inputJar, String hnpEntryName, Map<String, SignInfo> hnpLibSignInfoMap,
258         Map<String, String> hnpTypeMap, Pair<String, String> pairResult)
259         throws IOException, FsVerityDigestException, VerifyCodeSignException, CMSException {
260         JarEntry hnpEntry = inputJar.getJarEntry(hnpEntryName);
261         try (InputStream inputStream = inputJar.getInputStream(hnpEntry);
262             ZipInputStream hnpInputStream = new ZipInputStream(inputStream)) {
263             String hnpFileName = HapUtils.parseHnpPath(hnpEntryName);
264             if (!hnpTypeMap.containsKey(hnpFileName)) {
265                 throw new VerifyCodeSignException("hnp should be described in module.json");
266             }
267             String hnpType = hnpTypeMap.get(hnpFileName);
268             java.util.zip.ZipEntry libEntry = null;
269             while ((libEntry = hnpInputStream.getNextEntry()) != null) {
270                 String libPath = hnpEntry.getName() + "!/" + libEntry.getName();
271                 if (!hnpLibSignInfoMap.containsKey(libPath)) {
272                     continue;
273                 }
274                 LOGGER.info("verify lib: {}", libPath);
275                 SignInfo signInfo = hnpLibSignInfoMap.get(libPath);
276                 byte[] entrySig = signInfo.getSignature();
277                 long dataSize = signInfo.getDataSize();
278                 verifySingleFile(hnpInputStream, dataSize, entrySig, 0, null);
279                 checkHnpOwnerID(entrySig, pairResult.getFirst(), pairResult.getSecond(), hnpType);
280                 hnpInputStream.closeEntry();
281             }
282         }
283     }
284 
generateCodeSignBlock(File file, long offset, long length)285     private static CodeSignBlock generateCodeSignBlock(File file, long offset, long length)
286         throws IOException, VerifyCodeSignException {
287         CodeSignBlock csb = new CodeSignBlock();
288         // 1) parse sign block to CodeSignBlock object
289         try (FileInputStream signedHap = new FileInputStream(file)) {
290             int fileReadOffset = 0;
291             // 0) skip data part, but fileReadOffset remains at start(0)
292             signedHap.skip(offset);
293             // 1) parse codeSignBlockHeader
294             byte[] codeSignBlockHeaderByteArray = new byte[CodeSignBlockHeader.size()];
295             fileReadOffset += signedHap.read(codeSignBlockHeaderByteArray);
296             csb.setCodeSignBlockHeader(CodeSignBlockHeader.fromByteArray(codeSignBlockHeaderByteArray));
297             if (csb.getCodeSignBlockHeader().getBlockSize() != length) {
298                 throw new VerifyCodeSignException("Invalid code Sign block size of setCodeSignBlockHeader");
299             }
300             // 2) parse segment headers
301             for (int i = 0; i < csb.getCodeSignBlockHeader().getSegmentNum(); i++) {
302                 byte[] segmentHeaderByteArray = new byte[SegmentHeader.SEGMENT_HEADER_LENGTH];
303                 fileReadOffset += signedHap.read(segmentHeaderByteArray);
304                 csb.addToSegmentList(SegmentHeader.fromByteArray(segmentHeaderByteArray));
305             }
306             // compute merkle tree offset by alignment, based on file start
307             long computedTreeOffset = getAlignmentAddr(CodeSignBlock.PAGE_SIZE_4K, fileReadOffset + offset);
308             // skip zero padding before merkle tree, adds zero padding length to fileReadOffset
309             fileReadOffset += signedHap.skip(computedTreeOffset - offset - fileReadOffset);
310             parseMerkleTree(csb, fileReadOffset, signedHap, computedTreeOffset);
311         }
312         return csb;
313     }
314 
parseMerkleTree(CodeSignBlock csb, int readOffset, FileInputStream signedHap, long computedTreeOffset)315     private static void parseMerkleTree(CodeSignBlock csb, int readOffset, FileInputStream signedHap,
316         long computedTreeOffset) throws VerifyCodeSignException, IOException {
317         // check segment offset and segment size
318         byte[] merkleTreeBytes = new byte[0];
319         int fileReadOffset = readOffset;
320         for (SegmentHeader segmentHeader : csb.getSegmentHeaderList()) {
321             if (fileReadOffset > segmentHeader.getSegmentOffset()) {
322                 throw new VerifyCodeSignException("Invaild offset of merkle tree and segment header");
323             }
324             // get merkle tree bytes
325             if (fileReadOffset < segmentHeader.getSegmentOffset()) {
326                 merkleTreeBytes = new byte[segmentHeader.getSegmentOffset() - fileReadOffset];
327                 fileReadOffset += signedHap.read(merkleTreeBytes);
328             }
329             byte[] sh = new byte[segmentHeader.getSegmentSize()];
330             fileReadOffset += signedHap.read(sh);
331             if (segmentHeader.getType() == SegmentHeader.CSB_FSVERITY_INFO_SEG) {
332                 // 3) parse fs-verity info segment
333                 csb.setFsVerityInfoSegment(FsVerityInfoSegment.fromByteArray(sh));
334             } else if (segmentHeader.getType() == SegmentHeader.CSB_HAP_META_SEG) {
335                 // 4) parse hap info segment
336                 csb.setHapInfoSegment(HapInfoSegment.fromByteArray(sh));
337             } else if (segmentHeader.getType() == SegmentHeader.CSB_NATIVE_LIB_INFO_SEG) {
338                 // 5) parse so info segment
339                 csb.setSoInfoSegment(NativeLibInfoSegment.fromByteArray(sh));
340             }
341         }
342         if (fileReadOffset != csb.getCodeSignBlockHeader().getBlockSize()) {
343             throw new VerifyCodeSignException("Invalid blockSize of getCodeSignBlockHeader");
344         }
345         // parse merkle tree
346         Extension extension = csb.getHapInfoSegment()
347             .getSignInfo()
348             .getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED);
349         if (extension == null) {
350             throw new VerifyCodeSignException("Missing merkleTreeExtension in verifycation");
351         }
352         if (extension instanceof MerkleTreeExtension) {
353             MerkleTreeExtension mte = (MerkleTreeExtension) extension;
354             if (computedTreeOffset != mte.getMerkleTreeOffset()) {
355                 throw new VerifyCodeSignException("Invalid merkle tree offset");
356             }
357             if (merkleTreeBytes.length != mte.getMerkleTreeSize()) {
358                 throw new VerifyCodeSignException("Invalid merkle tree size");
359             }
360             csb.addOneMerkleTree(CodeSigning.HAP_SIGNATURE_ENTRY_NAME, merkleTreeBytes);
361         }
362     }
363 
getAlignmentAddr(long alignment, long input)364     private static long getAlignmentAddr(long alignment, long input) {
365         long residual = input % alignment;
366         if (residual == 0) {
367             return input;
368         } else {
369             return input + (alignment - residual);
370         }
371     }
372 
373     /**
374      * Verifies the signature of a given file with its signature in pkcs#7 format
375      *
376      * @param input             file being verified in InputStream representation
377      * @param length            size of signed data in the file
378      * @param signature         byte array of signature in pkcs#7 format
379      * @param merkleTreeOffset  merkle tree offset based on file start
380      * @param inMerkleTreeBytes merkle tree raw bytes
381      * @throws FsVerityDigestException if fs-verity digest generation failed
382      * @throws CMSException            if signature verify failed
383      * @throws VerifyCodeSignException parsing code sign block error
384      */
verifySingleFile(InputStream input, long length, byte[] signature, long merkleTreeOffset, byte[] inMerkleTreeBytes)385     public static void verifySingleFile(InputStream input, long length, byte[] signature, long merkleTreeOffset,
386         byte[] inMerkleTreeBytes) throws FsVerityDigestException, CMSException, VerifyCodeSignException {
387         Pair<byte[], byte[]> pairResult = null;
388         try {
389             pairResult = generateFsVerityDigest(input, length, merkleTreeOffset);
390         } catch (PageInfoException e) {
391             throw new VerifyCodeSignException(e.getMessage());
392         }
393         byte[] generatedMerkleTreeBytes = pairResult.getSecond();
394         if (generatedMerkleTreeBytes == null) {
395             generatedMerkleTreeBytes = new byte[0];
396         }
397         // For native libs, inMerkleTreeBytes is null, skip check here
398         if ((inMerkleTreeBytes != null) && !Arrays.equals(inMerkleTreeBytes, generatedMerkleTreeBytes)) {
399             throw new VerifyCodeSignException("verify merkle tree bytes failed");
400         }
401         CmsUtils.verifySignDataWithUnsignedDataDigest(pairResult.getFirst(), signature);
402     }
403 
generateFsVerityDigest(InputStream inputStream, long size, long merkleTreeOffset)404     private static Pair<byte[], byte[]> generateFsVerityDigest(InputStream inputStream, long size,
405         long merkleTreeOffset) throws FsVerityDigestException, PageInfoException {
406         FsVerityGenerator fsVerityGenerator = new FsVerityGenerator();
407         fsVerityGenerator.generateFsVerityDigest(inputStream, size, merkleTreeOffset);
408         return Pair.create(fsVerityGenerator.getFsVerityDigest(), fsVerityGenerator.getTreeBytes());
409     }
410 }
411