• 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.ElfSignBlock;
20 import com.ohos.hapsigntool.codesigning.datastructure.Extension;
21 import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment;
22 import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension;
23 import com.ohos.hapsigntool.codesigning.datastructure.SignInfo;
24 import com.ohos.hapsigntool.codesigning.exception.CodeSignException;
25 import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException;
26 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptor;
27 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptorWithSign;
28 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator;
29 import com.ohos.hapsigntool.codesigning.utils.HapUtils;
30 import com.ohos.hapsigntool.hap.config.SignerConfig;
31 import com.ohos.hapsigntool.entity.Pair;
32 import com.ohos.hapsigntool.error.HapFormatException;
33 import com.ohos.hapsigntool.error.ProfileException;
34 import com.ohos.hapsigntool.signer.LocalSigner;
35 import com.ohos.hapsigntool.utils.FileUtils;
36 import com.ohos.hapsigntool.utils.StringUtils;
37 import com.ohos.hapsigntool.zip.UnsignedDecimalUtil;
38 import com.ohos.hapsigntool.zip.Zip;
39 import com.ohos.hapsigntool.zip.ZipEntryHeader;
40 import com.ohos.hapsigntool.zip.ZipEntry;
41 
42 import org.apache.logging.log4j.LogManager;
43 import org.apache.logging.log4j.Logger;
44 
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.nio.ByteBuffer;
50 import java.nio.ByteOrder;
51 import java.util.ArrayList;
52 import java.util.Enumeration;
53 import java.util.List;
54 import java.util.Locale;
55 import java.util.jar.JarEntry;
56 import java.util.jar.JarFile;
57 import java.util.regex.Matcher;
58 import java.util.regex.Pattern;
59 
60 /**
61  * core functions of code signing
62  *
63  * @since 2023/06/05
64  */
65 public class CodeSigning {
66     /**
67      * Only hap and hsp bundle supports code signing
68      */
69     public static final String[] SUPPORT_FILE_FORM = {"hap", "hsp"};
70 
71     /**
72      * Only elf file supports bin code signing
73      */
74     public static final String SUPPORT_BIN_FILE_FORM = "elf";
75 
76     /**
77      * Defined entry name of hap file
78      */
79     public static final String HAP_SIGNATURE_ENTRY_NAME = "Hap";
80 
81     private static final Logger LOGGER = LogManager.getLogger(CodeSigning.class);
82 
83     private static final String NATIVE_LIB_AN_SUFFIX = ".an";
84 
85     private static final String NATIVE_LIB_SO_SUFFIX = ".so";
86 
87     private final List<String> extractedNativeLibSuffixs = new ArrayList<>();
88 
89     private final SignerConfig signConfig;
90 
91     private CodeSignBlock codeSignBlock;
92 
93     private long timestamp = 0L;
94 
95     /**
96      * provide code sign functions to sign a hap
97      *
98      * @param signConfig configuration of sign
99      */
CodeSigning(SignerConfig signConfig)100     public CodeSigning(SignerConfig signConfig) {
101         this.signConfig = signConfig;
102     }
103 
104     /**
105      * Sign the given elf file, and pack all signature into output file
106      *
107      * @param input  file to sign
108      * @param offset position of codesign block based on start of the file
109      * @param inForm file's format
110      * @param profileContent profile of the elf
111      * @return byte array of code sign block
112      * @throws CodeSignException        code signing exception
113      * @throws IOException              io error
114      * @throws FsVerityDigestException  computing FsVerity digest error
115      * @throws ProfileException         profile of elf is invalid
116      */
getElfCodeSignBlock(File input, long offset, String inForm, String profileContent)117     public byte[] getElfCodeSignBlock(File input, long offset, String inForm, String profileContent)
118         throws CodeSignException, FsVerityDigestException, IOException, ProfileException {
119         if (!SUPPORT_BIN_FILE_FORM.equalsIgnoreCase(inForm)) {
120             throw new CodeSignException("file's format is unsupported");
121         }
122         long fileSize = input.length();
123         int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset);
124         long fsvTreeOffset = offset + Integer.BYTES * 2 + paddingSize;
125         try (FileInputStream inputStream = new FileInputStream(input)) {
126             FsVerityGenerator fsVerityGenerator = new FsVerityGenerator();
127             fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset);
128             byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest();
129             // ownerID should be DEBUG_LIB_ID while signing ELF
130             String ownerID = (profileContent == null) ? "DEBUG_LIB_ID" : HapUtils.getAppIdentifier(profileContent);
131             byte[] signature = generateSignature(fsVerityDigest, ownerID);
132             // add fs-verify info
133             FsVerityDescriptor.Builder fsdbuilder = new FsVerityDescriptor.Builder().setFileSize(fileSize)
134                 .setHashAlgorithm(FsVerityGenerator.getFsVerityHashAlgorithm())
135                 .setLog2BlockSize(FsVerityGenerator.getLog2BlockSize())
136                 .setSaltSize((byte) fsVerityGenerator.getSaltSize())
137                 .setSignSize(signature.length)
138                 .setFileSize(fileSize)
139                 .setSalt(fsVerityGenerator.getSalt())
140                 .setRawRootHash(fsVerityGenerator.getRootHash())
141                 .setFlags(FsVerityDescriptor.FLAG_STORE_MERKLE_TREE_OFFSET)
142                 .setMerkleTreeOffset(fsvTreeOffset)
143                 .setCsVersion(FsVerityDescriptor.CODE_SIGN_VERSION);
144             FsVerityDescriptorWithSign fsVerityDescriptorWithSign = new FsVerityDescriptorWithSign(fsdbuilder.build(),
145                 signature);
146             byte[] treeBytes = fsVerityGenerator.getTreeBytes();
147             ElfSignBlock signBlock = new ElfSignBlock(paddingSize, treeBytes, fsVerityDescriptorWithSign);
148             LOGGER.info("Sign elf successfully.");
149             return signBlock.toByteArray();
150         }
151     }
152 
153     /**
154      * Sign the given hap file, and pack all signature into output file
155      *
156      * @param input  file to sign
157      * @param offset position of codesign block based on start of the file
158      * @param inForm file's format
159      * @param profileContent profile of the hap
160      * @param zip zip
161      * @return byte array of code sign block
162      * @throws CodeSignException        code signing exception
163      * @throws IOException              io error
164      * @throws HapFormatException       hap format invalid
165      * @throws FsVerityDigestException  computing FsVerity digest error
166      * @throws ProfileException         profile of the hap error
167      */
getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip)168     public byte[] getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip)
169         throws CodeSignException, IOException, HapFormatException, FsVerityDigestException, ProfileException {
170         LOGGER.info("Start to sign code.");
171         if (!StringUtils.containsIgnoreCase(SUPPORT_FILE_FORM, inForm)) {
172             throw new CodeSignException("file's format is unsupported");
173         }
174         long dataSize = computeDataSize(zip);
175         timestamp = System.currentTimeMillis();
176         // generate CodeSignBlock
177         this.codeSignBlock = new CodeSignBlock();
178         // compute merkle tree offset, replace with computeMerkleTreeOffset if fs-verity descriptor supports
179         long fsvTreeOffset = this.codeSignBlock.computeMerkleTreeOffset(offset);
180         // update fs-verity segment
181         FsVerityInfoSegment fsVerityInfoSegment = new FsVerityInfoSegment(FsVerityDescriptor.VERSION,
182             FsVerityGenerator.getFsVerityHashAlgorithm(), FsVerityGenerator.getLog2BlockSize());
183         this.codeSignBlock.setFsVerityInfoSegment(fsVerityInfoSegment);
184 
185         LOGGER.debug("Sign hap.");
186         String ownerID = HapUtils.getAppIdentifier(profileContent);
187 
188         try (FileInputStream inputStream = new FileInputStream(input)) {
189             Pair<SignInfo, byte[]> hapSignInfoAndMerkleTreeBytesPair = signFile(inputStream, dataSize, true,
190                 fsvTreeOffset, ownerID);
191             // update hap segment in CodeSignBlock
192             this.codeSignBlock.getHapInfoSegment().setSignInfo(hapSignInfoAndMerkleTreeBytesPair.getFirst());
193             // Insert merkle tree bytes into code sign block
194             this.codeSignBlock.addOneMerkleTree(HAP_SIGNATURE_ENTRY_NAME,
195                 hapSignInfoAndMerkleTreeBytesPair.getSecond());
196         }
197         // update native lib info segment in CodeSignBlock
198         signNativeLibs(input, ownerID);
199 
200         // last update codeSignBlock before generating its byte array representation
201         updateCodeSignBlock(this.codeSignBlock);
202 
203         // complete code sign block byte array here
204         byte[] generated = this.codeSignBlock.generateCodeSignBlockByte(fsvTreeOffset);
205         LOGGER.info("Sign successfully.");
206         return generated;
207     }
208 
computeDataSize(Zip zip)209     private long computeDataSize(Zip zip) throws HapFormatException {
210         long dataSize = 0L;
211         for (ZipEntry entry : zip.getZipEntries()) {
212             ZipEntryHeader zipEntryHeader = entry.getZipEntryData().getZipEntryHeader();
213             if (FileUtils.isRunnableFile(zipEntryHeader.getFileName())
214                 && zipEntryHeader.getMethod() == Zip.FILE_UNCOMPRESS_METHOD_FLAG) {
215                 continue;
216             }
217             // if the first file is not uncompressed abc or so, set dataSize to zero
218             if (entry.getCentralDirectory().getOffset() == 0) {
219                 break;
220             }
221             // the first entry which is not abc/so/an is found, return its data offset
222             dataSize = entry.getCentralDirectory().getOffset() + ZipEntryHeader.HEADER_LENGTH
223                     + zipEntryHeader.getFileNameLength() + zipEntryHeader.getExtraLength();
224             break;
225         }
226         if ((dataSize % CodeSignBlock.PAGE_SIZE_4K) != 0) {
227             throw new HapFormatException(
228                 String.format(Locale.ROOT, "Invalid dataSize(%d), not a multiple of 4096", dataSize));
229         }
230         return dataSize;
231     }
232 
signNativeLibs(File input, String ownerID)233     private void signNativeLibs(File input, String ownerID) throws IOException, FsVerityDigestException,
234             CodeSignException {
235         // 'an' libs are always signed
236         extractedNativeLibSuffixs.add(NATIVE_LIB_AN_SUFFIX);
237         // 'so' libs are always signed
238         extractedNativeLibSuffixs.add(NATIVE_LIB_SO_SUFFIX);
239 
240         // sign native files
241         try (JarFile inputJar = new JarFile(input, false)) {
242             List<String> entryNames = getNativeEntriesFromHap(inputJar);
243             if (entryNames.isEmpty()) {
244                 LOGGER.info("No native libs.");
245                 return;
246             }
247             List<Pair<String, SignInfo>> nativeLibInfoList = signFilesFromJar(entryNames, inputJar, ownerID);
248             // update SoInfoSegment in CodeSignBlock
249             this.codeSignBlock.getSoInfoSegment().setSoInfoList(nativeLibInfoList);
250         }
251     }
252 
253     /**
254      * Get entry name of all native files in hap
255      *
256      * @param hap the given hap
257      * @return list of entry name
258      */
getNativeEntriesFromHap(JarFile hap)259     private List<String> getNativeEntriesFromHap(JarFile hap) {
260         List<String> result = new ArrayList<>();
261         for (Enumeration<JarEntry> e = hap.entries(); e.hasMoreElements();) {
262             JarEntry entry = e.nextElement();
263             if (!entry.isDirectory()) {
264                 if (!isNativeFile(entry.getName())) {
265                     continue;
266                 }
267                 result.add(entry.getName());
268             }
269         }
270         return result;
271     }
272 
273     /**
274      * Check whether the entry is a native file
275      *
276      * @param entryName the name of entry
277      * @return true if it is a native file, and false otherwise
278      */
isNativeFile(String entryName)279     private boolean isNativeFile(String entryName) {
280         if (StringUtils.isEmpty(entryName)) {
281             return false;
282         }
283         if (entryName.endsWith(NATIVE_LIB_AN_SUFFIX)) {
284             return true;
285         }
286         if (extractedNativeLibSuffixs.contains(NATIVE_LIB_SO_SUFFIX)) {
287             Pattern pattern = FileUtils.SUFFIX_REGEX_MAP.get("so");
288             Matcher matcher = pattern.matcher(entryName);
289             if (matcher.find()) {
290                 return true;
291             }
292         }
293         return false;
294     }
295 
296     /**
297      * Sign specific entries in a hap
298      *
299      * @param entryNames list of entries which need to be signed
300      * @param hap        input hap
301      * @param ownerID    app-id in signature to identify
302      * @return sign info and merkle tree of each file
303      * @throws IOException             io error
304      * @throws FsVerityDigestException computing FsVerity digest error
305      * @throws CodeSignException       sign error
306      */
signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID)307     private List<Pair<String, SignInfo>> signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID)
308         throws IOException, FsVerityDigestException, CodeSignException {
309         List<Pair<String, SignInfo>> nativeLibInfoList = new ArrayList<>();
310         for (String name : entryNames) {
311             LOGGER.debug("Sign entry name = " + name);
312             JarEntry inEntry = hap.getJarEntry(name);
313             try (InputStream inputStream = hap.getInputStream(inEntry)) {
314                 long fileSize = inEntry.getSize();
315                 // We don't store merkle tree in code signing of native libs
316                 // Therefore, the second value of pair returned is ignored
317                 Pair<SignInfo, byte[]> pairSignInfoAndMerkleTreeBytes = signFile(inputStream, fileSize,
318                         false, 0, ownerID);
319                 nativeLibInfoList.add(Pair.create(name, pairSignInfoAndMerkleTreeBytes.getFirst()));
320             }
321         }
322         return nativeLibInfoList;
323     }
324 
325     /**
326      * Sign a file from input stream
327      *
328      * @param inputStream   input stream of a file
329      * @param fileSize      size of the file
330      * @param storeTree     whether to store merkle tree in signed info
331      * @param fsvTreeOffset merkle tree raw bytes offset based on the start of file
332      * @param ownerID       app-id in signature to identify
333      * @return pair of signature and tree
334      * @throws FsVerityDigestException computing FsVerity Digest error
335      * @throws CodeSignException       signing error
336      */
signFile(InputStream inputStream, long fileSize, boolean storeTree, long fsvTreeOffset, String ownerID)337     public Pair<SignInfo, byte[]> signFile(InputStream inputStream, long fileSize, boolean storeTree,
338         long fsvTreeOffset, String ownerID) throws FsVerityDigestException, CodeSignException {
339         FsVerityGenerator fsVerityGenerator = new FsVerityGenerator();
340         fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset);
341         byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest();
342         byte[] signature = generateSignature(fsVerityDigest, ownerID);
343         int flags = 0;
344         if (storeTree) {
345             flags = SignInfo.FLAG_MERKLE_TREE_INCLUDED;
346         }
347         SignInfo signInfo = new SignInfo(fsVerityGenerator.getSaltSize(), flags, fileSize, fsVerityGenerator.getSalt(),
348             signature);
349         // if store merkle tree in sign info
350         if (storeTree) {
351             int merkleTreeSize = fsVerityGenerator.getTreeBytes() == null ? 0 : fsVerityGenerator.getTreeBytes().length;
352             Extension merkleTreeExtension = new MerkleTreeExtension(merkleTreeSize, fsvTreeOffset,
353                 fsVerityGenerator.getRootHash());
354             signInfo.addExtension(merkleTreeExtension);
355         }
356         return Pair.create(signInfo, fsVerityGenerator.getTreeBytes());
357     }
358 
generateSignature(byte[] signedData, String ownerID)359     private byte[] generateSignature(byte[] signedData, String ownerID) throws CodeSignException {
360         // signConfig is created by SignerFactory
361         if ((signConfig.getSigner() instanceof LocalSigner)) {
362             if (signConfig.getCertificates().isEmpty()) {
363                 throw new CodeSignException("No certificates configured for sign");
364             }
365         }
366 
367         BcSignedDataGenerator bcSignedDataGenerator = new BcSignedDataGenerator();
368         bcSignedDataGenerator.setOwnerID(ownerID);
369         return bcSignedDataGenerator.generateSignedData(signedData, signConfig);
370     }
371 
372     /**
373      * At here, segment header, fsverity info/hap/so info segment, merkle tree
374      * segment should all be generated.
375      * code sign block size, segment number, offset is not updated.
376      * Try to update whatever could be updated here.
377      *
378      * @param codeSignBlock CodeSignBlock
379      */
updateCodeSignBlock(CodeSignBlock codeSignBlock)380     private void updateCodeSignBlock(CodeSignBlock codeSignBlock) {
381         // construct segment header list
382         codeSignBlock.setSegmentHeaders();
383         // Compute and set segment number
384         codeSignBlock.setSegmentNum();
385         // update code sign block header flag
386         codeSignBlock.setCodeSignBlockFlag();
387         // compute segment offset
388         codeSignBlock.computeSegmentOffset();
389     }
390 
parseCentralDirectory(byte[] buffer, int count)391     private List<CentralDirectory> parseCentralDirectory(byte[] buffer, int count) {
392         List<CentralDirectory> cdList = new ArrayList<>();
393         ByteBuffer cdBuffer = ByteBuffer.allocate(buffer.length).order(ByteOrder.LITTLE_ENDIAN);
394         cdBuffer.put(buffer);
395         cdBuffer.rewind();
396         for (int i = 0; i < count; i++) {
397             byte[] bytesBeforeCompressionMethod = new byte[CentralDirectory.BYTE_SIZE_BEFORE_COMPRESSION_METHOD];
398             cdBuffer.get(bytesBeforeCompressionMethod);
399             char compressionMode = cdBuffer.getChar();
400             CentralDirectory.Builder builder = new CentralDirectory.Builder().setCompressionMethod(compressionMode);
401             byte[] bytesBetweenCmprMethodAndFileNameLength =
402                     new byte[CentralDirectory.BYTE_SIZE_BETWEEN_COMPRESSION_MODE_AND_FILE_SIZE];
403             cdBuffer.get(bytesBetweenCmprMethodAndFileNameLength);
404             char fileNameLength = cdBuffer.getChar();
405             char extraFieldLength = cdBuffer.getChar();
406             char fileCommentLength = cdBuffer.getChar();
407             byte[] attributes =
408                     new byte[CentralDirectory.BYTE_SIZE_BETWEEN_FILE_COMMENT_LENGTH_AND_LOCHDR_RELATIVE_OFFSET];
409             cdBuffer.get(attributes);
410             long locHdrOffset = UnsignedDecimalUtil.getUnsignedInt(cdBuffer);
411             builder.setFileNameLength(fileNameLength).setExtraFieldLength(extraFieldLength)
412                 .setFileCommentLength(fileCommentLength).setRelativeOffsetOfLocalHeader(locHdrOffset);
413             byte[] fileNameBuffer = new byte[fileNameLength];
414             cdBuffer.get(fileNameBuffer);
415             if (extraFieldLength != 0) {
416                 cdBuffer.get(new byte[extraFieldLength]);
417             }
418             if (fileCommentLength != 0) {
419                 cdBuffer.get(new byte[fileCommentLength]);
420             }
421             CentralDirectory cd = builder.setFileName(fileNameBuffer).build();
422             cdList.add(cd);
423         }
424 
425         return cdList;
426     }
427 }
428