diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/CodeSignBlock.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/CodeSignBlock.java new file mode 100644 index 0000000000000000000000000000000000000000..40679e46f2debe76c55a50d57980aaad9cd708d4 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/CodeSignBlock.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.sign.CodeSigning; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * code sign block is a chunk of bytes attached to hap package or file. + * It consists of two headers: + * 1) code sign block header + * 2) segment header + * three segments: + * 1) fs-verity info segment + * 2) hap info segment + * 3) so info segment + * one zero padding area in order to align merkle tree raw bytes + * 1) zero padding + * and one area storing merkle tree bytes: + * 1) merkle tree raw bytes + *

+ * After signing a hap, call toByteArray() method to generate a block of bytes. + * + * @since 2023/09/08 + */ +public class CodeSignBlock { + /** + * page size in bytes + */ + public static final long PAGE_SIZE_4K = 4096L; + + /** + * Segment header count, including fs-verity info, hap info, so info segment + */ + public static final int SEGMENT_HEADER_COUNT = 3; + + private CodeSignBlockHeader codeSignBlockHeader; + + private final List segmentHeaderList; + + private FsVerityInfoSegment fsVerityInfoSegment; + + private HapInfoSegment hapInfoSegment; + + private NativeLibInfoSegment nativeLibInfoSegment; + + private byte[] zeroPadding; + + private final Map merkleTreeMap; + + public CodeSignBlock() { + this.codeSignBlockHeader = new CodeSignBlockHeader.Builder().build(); + this.segmentHeaderList = new ArrayList<>(); + this.fsVerityInfoSegment = new FsVerityInfoSegment(); + this.hapInfoSegment = new HapInfoSegment(); + this.nativeLibInfoSegment = new NativeLibInfoSegment.Builder().build(); + this.merkleTreeMap = new HashMap<>(); + } + + /** + * Add one merkle tree into merkleTreeMap + * + * @param key file name + * @param merkleTree merkle tree raw bytes + */ + public void addOneMerkleTree(String key, byte[] merkleTree) { + if (merkleTree == null) { + this.merkleTreeMap.put(key, new byte[0]); + } else { + this.merkleTreeMap.put(key, merkleTree); + } + } + + /** + * Get one merkle tree from merkleTreeMap by file name + * + * @param key file name + * @return merkle tree bytes + */ + public byte[] getOneMerkleTreeByFileName(String key) { + return this.merkleTreeMap.get(key); + } + + /** + * set code sign block flag + */ + public void setCodeSignBlockFlag() { + int flags = CodeSignBlockHeader.FLAG_MERKLE_TREE_INLINED; + if (this.nativeLibInfoSegment.getSectionNum() != 0) { + flags += CodeSignBlockHeader.FLAG_NATIVE_LIB_INCLUDED; + } + this.codeSignBlockHeader.setFlags(flags); + } + + /** + * set segmentNum defined in code sign block header, equals length of segmentHeaderList + */ + public void setSegmentNum() { + this.codeSignBlockHeader.setSegmentNum(segmentHeaderList.size()); + } + + /** + * add one segment to segmentHeaderList + * + * @param sh segment header + */ + public void addToSegmentList(SegmentHeader sh) { + this.segmentHeaderList.add(sh); + } + + public List getSegmentHeaderList() { + return segmentHeaderList; + } + + /** + * set segment header list + */ + public void setSegmentHeaders() { + // fs-verity info segment + segmentHeaderList.add(new SegmentHeader(SegmentHeader.CSB_FSVERITY_INFO_SEG, this.fsVerityInfoSegment.size())); + // hap info segment + segmentHeaderList.add(new SegmentHeader(SegmentHeader.CSB_HAP_META_SEG, this.hapInfoSegment.size())); + // native lib info segment + segmentHeaderList.add( + new SegmentHeader(SegmentHeader.CSB_NATIVE_LIB_INFO_SEG, this.nativeLibInfoSegment.size())); + } + + public CodeSignBlockHeader getCodeSignBlockHeader() { + return codeSignBlockHeader; + } + + public void setCodeSignBlockHeader(CodeSignBlockHeader csbHeader) { + this.codeSignBlockHeader = csbHeader; + } + + public void setFsVerityInfoSegment(FsVerityInfoSegment fsVeritySeg) { + this.fsVerityInfoSegment = fsVeritySeg; + } + + public FsVerityInfoSegment getFsVerityInfoSegment() { + return fsVerityInfoSegment; + } + + public HapInfoSegment getHapInfoSegment() { + return hapInfoSegment; + } + + public void setHapInfoSegment(HapInfoSegment hapSeg) { + this.hapInfoSegment = hapSeg; + } + + public NativeLibInfoSegment getSoInfoSegment() { + return nativeLibInfoSegment; + } + + public void setSoInfoSegment(NativeLibInfoSegment soSeg) { + this.nativeLibInfoSegment = soSeg; + } + + /** + * Convert code sign block object to a newly created byte array + * + * @return Byte array representation of a CodeSignBlock object + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(this.codeSignBlockHeader.getBlockSize()).order(ByteOrder.LITTLE_ENDIAN); + bf.put(this.codeSignBlockHeader.toByteArray()); + for (SegmentHeader sh : this.segmentHeaderList) { + bf.put(sh.toByteArray()); + } + bf.put(this.zeroPadding); + // Hap merkle tree + if (this.hapInfoSegment.getSignInfo().getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED) != null) { + bf.put(merkleTreeMap.get("Hap")); + } + bf.put(this.fsVerityInfoSegment.toByteArray()); + bf.put(this.hapInfoSegment.toByteArray()); + bf.put(this.nativeLibInfoSegment.toByteArray()); + return bf.array(); + } + + /** + * SegmentOffset is the position of each segment defined in segmentHeaderList, + * based on the start position of code sign block + */ + public void computeSegmentOffset() { + // 1) the first segment is placed after merkle tree + int segmentOffset = CodeSignBlockHeader.size() + + this.segmentHeaderList.size() * SegmentHeader.SEGMENT_HEADER_LENGTH + this.zeroPadding.length + + this.getOneMerkleTreeByFileName(CodeSigning.HAP_SIGNATURE_ENTRY_NAME).length; + for (SegmentHeader sh : segmentHeaderList) { + sh.setSegmentOffset(segmentOffset); + segmentOffset += sh.getSegmentSize(); + } + } + + /** + * Compute the offset to store merkle tree raw bytes based on file start + * + * @param codeSignBlockOffset offset to store code sign block based on file start + * @return offset to store merkle tree based on the file start, it includes the codeSignBlockOffset + */ + public long computeMerkleTreeOffset(long codeSignBlockOffset) { + long sizeWithoutMerkleTree = CodeSignBlockHeader.size() + + SEGMENT_HEADER_COUNT * SegmentHeader.SEGMENT_HEADER_LENGTH; + // add code sign block offset while computing align position for merkle tree + long residual = (codeSignBlockOffset + sizeWithoutMerkleTree) % PAGE_SIZE_4K; + if (residual == 0) { + this.zeroPadding = new byte[0]; + } else { + this.zeroPadding = new byte[(int) (PAGE_SIZE_4K - residual)]; + } + return codeSignBlockOffset + sizeWithoutMerkleTree + zeroPadding.length; + } + + /** + * Convert CodeSignBlock to bytes + * + * @param fsvTreeOffset merkle tree offset + * @return byte array representing the code sign block + */ + public byte[] generateCodeSignBlockByte(long fsvTreeOffset) { + // 1) compute overall block size without merkle tree + long csbSize = CodeSignBlockHeader.size() + + (long) this.segmentHeaderList.size() * SegmentHeader.SEGMENT_HEADER_LENGTH + this.zeroPadding.length + + this.getOneMerkleTreeByFileName(CodeSigning.HAP_SIGNATURE_ENTRY_NAME).length + + this.fsVerityInfoSegment.size() + this.hapInfoSegment.size() + this.nativeLibInfoSegment.size(); + Extension ext = this.hapInfoSegment.getSignInfo().getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED); + if (ext instanceof MerkleTreeExtension) { + MerkleTreeExtension merkleTreeExtension = (MerkleTreeExtension) ext; + merkleTreeExtension.setMerkleTreeOffset(fsvTreeOffset); + } + this.codeSignBlockHeader.setBlockSize(csbSize); + // 2) generate byte array of complete code sign block + return toByteArray(); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, + "CodeSignBlockHeader[%s], SegmentHeaders[%s], FsVeritySeg[%s], HapInfoSeg[%s], SoInfoSeg[%s]", + this.codeSignBlockHeader, Arrays.toString(this.segmentHeaderList.toArray()), this.fsVerityInfoSegment, + this.hapInfoSegment, this.nativeLibInfoSegment); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/CodeSignBlockHeader.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/CodeSignBlockHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..8a1572fa42747aa2bb2c68e92ca570a3e6ec5fb7 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/CodeSignBlockHeader.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Code sign block header + *

+ * Structure: + * 1) u64 magic: magic number + * 2) u32 version: sign tool version + * 3) u32 blockSize: size of code sign block + * 4) u32 segmentNum: number of segments, i.e. FsVerityInfoSegment, HapInfoSegment, SoInfoSegment + * 5) u32 flags + * 6) u8[8] reserved: for reservation + *

+ * The Size of Code sign Block header if fixed, getBlockLength() method returns the size. + * + * @since 2023/09/08 + */ +public class CodeSignBlockHeader { + /** + * Flag indicating that merkle tree is included in code sign block + */ + public static final int FLAG_MERKLE_TREE_INLINED = 0x1; + + /** + * Flag indicating that native lib is included in code sign block + */ + public static final int FLAG_NATIVE_LIB_INCLUDED = 0x2; + + // code signing version + private static final int CODE_SIGNING_VERSION = 1; + + // byte size of magic number + private static final byte MAGIC_BYTE_ARRAY_LENGTH = Long.BYTES; + + // lower 8 bytes of MD5 result of string "hap code sign block" (E046 C8C6 5389 FCCD) + private static final long MAGIC_NUM = ((0xE046C8C6L << 32) + 0x5389FCCDL); + + // size of byte[8] reserved + private static final byte RESERVED_BYTE_ARRAY_LENGTH = 8; + + // At all times three segment are always included in code sign block, update this if new segments are created. + private static final int SEGMENT_NUM = 3; + + private long magic; + + private int version; + + private int blockSize; + + private int segmentNum; + + private int flags; + + private byte[] reserved; + + /** + * Construct of CodeSignBlockHeader + * + * @param builder builder + */ + private CodeSignBlockHeader(Builder builder) { + this.magic = builder.magic; + this.version = builder.version; + this.blockSize = builder.blockSize; + this.segmentNum = builder.segmentNum; + this.flags = builder.flags; + this.reserved = builder.reserved; + } + + public void setSegmentNum(int num) { + this.segmentNum = num; + } + + public int getSegmentNum() { + return segmentNum; + } + + public void setBlockSize(long size) { + this.blockSize = (int) size; + } + + public int getBlockSize() { + return blockSize; + } + + public void setFlags(int flags) { + this.flags = flags; + } + + /** + * Converts code sign block headers to a newly created byte array + * + * @return Byte array representation of a code sign block header + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(size()).order(ByteOrder.LITTLE_ENDIAN); + bf.putLong(magic); + bf.putInt(version); + bf.putInt(blockSize); + bf.putInt(segmentNum); + bf.putInt(flags); + bf.put(reserved); + return bf.array(); + } + + /** + * Init the CodeSignBlockHeader by a byte array + * + * @param bytes Byte array representation of a CodeSignBlockHeader object + * @return a newly created CodeSignBlockHeader object + * @throws VerifyCodeSignException parse result invalid + */ + public static CodeSignBlockHeader fromByteArray(byte[] bytes) throws VerifyCodeSignException { + if (bytes.length != size()) { + throw new VerifyCodeSignException("Invalid size of CodeSignBlockHeader"); + } + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + // after put, rewind is mandatory before get + bf.rewind(); + long inMagic = bf.getLong(); + if (inMagic != MAGIC_NUM) { + throw new VerifyCodeSignException("Invalid magic num of CodeSignBlockHeader"); + } + int inVersion = bf.getInt(); + if (inVersion != CODE_SIGNING_VERSION) { + throw new VerifyCodeSignException("Invalid version of CodeSignBlockHeader"); + } + int inBlockSize = bf.getInt(); + int inSegmentNum = bf.getInt(); + if (inSegmentNum != SEGMENT_NUM) { + throw new VerifyCodeSignException("Invalid segmentNum of CodeSignBlockHeader"); + } + int inFlags = bf.getInt(); + if (inFlags < 0 || inFlags > (FLAG_MERKLE_TREE_INLINED + FLAG_NATIVE_LIB_INCLUDED)) { + throw new VerifyCodeSignException("Invalid flags of CodeSignBlockHeader"); + } + byte[] inReserved = new byte[RESERVED_BYTE_ARRAY_LENGTH]; + bf.get(inReserved); + return new Builder().setMagic(inMagic).setVersion(inVersion).setBlockSize(inBlockSize) + .setSegmentNum(inSegmentNum).setFlags(inFlags).setReserved(inReserved).build(); + } + + /** + * Return the byte size of code sign block header + * + * @return byte size of code sign block header + */ + public static int size() { + return MAGIC_BYTE_ARRAY_LENGTH + Integer.BYTES * 4 + RESERVED_BYTE_ARRAY_LENGTH; + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, + "CodeSignBlockHeader{magic: %d, version: %d, blockSize: %d, segmentNum: %d, flags: %d}", this.magic, + this.version, this.blockSize, this.segmentNum, this.flags); + } + + /** + * Builder of CodeSignBlockHeader class + */ + public static class Builder { + private long magic = MAGIC_NUM; + + private int version = CODE_SIGNING_VERSION; + + private int blockSize; + + private int segmentNum; + + private int flags; + + private byte[] reserved = new byte[RESERVED_BYTE_ARRAY_LENGTH]; + + public Builder setMagic(long magic) { + this.magic = magic; + return this; + } + + public Builder setVersion(int version) { + this.version = version; + return this; + } + + public Builder setBlockSize(int blockSize) { + this.blockSize = blockSize; + return this; + } + + public Builder setSegmentNum(int segmentNum) { + this.segmentNum = segmentNum; + return this; + } + + public Builder setFlags(int flags) { + this.flags = flags; + return this; + } + + public Builder setReserved(byte[] reserved) { + this.reserved = reserved; + return this; + } + + /** + * Create a CodeSignBlockHeader object + * + * @return a CodeSignBlockHeader object + */ + public CodeSignBlockHeader build() { + return new CodeSignBlockHeader(this); + } + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/ElfSignBlock.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/ElfSignBlock.java new file mode 100644 index 0000000000000000000000000000000000000000..27414edf9bbd4c91021ef866fd3714cfe8df3a90 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/ElfSignBlock.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptor; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptorWithSign; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * elf sign block is a chunk of bytes attached to elf file. + * 1) u32 type: 0x2 merkle tree + * 2) u32 length: merkle tree with padding size + * 3) u8[] merkle tree data + * 4) u32 type: 0x1 fsverity descriptor + * 5) u32 length: fsverity descriptor size + * 6) u8 version: fs-verity version + * 7) u8 hashAlgorithm: hash algorithm to use for the Merkle tree + * 8) u8 log2BlockSize: log2 of size of data and tree blocks + * 9) u8 saltSize: byte size of salt + * 10) u32 signSize: byte size of signature + * 11) u64 dataSize: byte size of data being signed + * 12) u8[64] rootHash: merkle tree root hash + * 13) u8[32] salt: salt used in signing + * 14) u32 flags + * 15) u32 reserved + * 16) u64 treeOffset: merkle tree offset + * 17) u8[128] reserved + * 18) u8 csVersion: code sign version + * 19) u8[] signature: signature after signing the data in byte array representation + * + * @since 2023/09/08 + */ +public class ElfSignBlock { + /** + * page size in bytes + */ + public static final int PAGE_SIZE_4K = 4096; + + /** + * Type of MerkleTree + */ + public static final int MERKLE_TREE_INLINED = 0x2; + + private int type = MERKLE_TREE_INLINED; + + private int treeLength; + + private byte[] merkleTreeWithPadding; + + private FsVerityDescriptorWithSign descriptorWithSign; + + /** + * Constructor of ElfSignBlock + * + * @param paddingSize padding size before merkle tree + * @param merkleTreeData merkle tree data + * @param descriptorWithSign FsVerityDescriptorWithSign object + */ + public ElfSignBlock(int paddingSize, byte[] merkleTreeData, FsVerityDescriptorWithSign descriptorWithSign) { + byte[] inMerkleTreeData = new byte[0]; + if (merkleTreeData != null){ + inMerkleTreeData = merkleTreeData; + } + this.treeLength = paddingSize + inMerkleTreeData.length; + this.merkleTreeWithPadding = new byte[this.treeLength]; + System.arraycopy(inMerkleTreeData, 0, merkleTreeWithPadding, paddingSize, inMerkleTreeData.length); + this.descriptorWithSign = descriptorWithSign; + } + + private ElfSignBlock(int type, int treeLength, byte[] merkleTreeWithPadding, + FsVerityDescriptorWithSign descriptorWithSign) { + this.type = type; + this.treeLength = treeLength; + this.merkleTreeWithPadding = merkleTreeWithPadding; + this.descriptorWithSign = descriptorWithSign; + } + + /** + * Return the byte size of code sign block + * + * @return byte size of code sign block + */ + public int size() { + return Integer.BYTES * 2 + merkleTreeWithPadding.length + descriptorWithSign.size(); + } + + /** + * return padding length by the sign block offset + * + * @param signBlockOffset sign block offset based on the start of file + * @return merkle tree raw bytes offset based on the start of file + */ + public static int computeMerkleTreePaddingLength(long signBlockOffset) { + return (int) (PAGE_SIZE_4K - (signBlockOffset + Integer.BYTES * 2) % PAGE_SIZE_4K); + } + + public byte[] getMerkleTreeWithPadding() { + return merkleTreeWithPadding; + } + + /** + * get DataSize + * + * @return DataSize + */ + public long getDataSize() { + return descriptorWithSign.getFsVerityDescriptor().getFileSize(); + } + + /** + * get TreeOffset + * + * @return TreeOffset + */ + public long getTreeOffset() { + return descriptorWithSign.getFsVerityDescriptor().getMerkleTreeOffset(); + } + + /** + * get Signature + * + * @return Signature + */ + public byte[] getSignature() { + return descriptorWithSign.getSignature(); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + * @throws FsVerityDigestException if error + */ + public byte[] toByteArray() throws FsVerityDigestException { + ByteBuffer bf = ByteBuffer.allocate(size()).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(type); + bf.putInt(merkleTreeWithPadding.length); + bf.put(merkleTreeWithPadding); + bf.put(descriptorWithSign.toByteArray()); + return bf.array(); + } + + /** + * Init the ElfSignBlock by a byte array + * + * @param bytes Byte array representation of a ElfSignBlock object + * @return a newly created ElfSignBlock object + * @throws VerifyCodeSignException parse result invalid + */ + public static ElfSignBlock fromByteArray(byte[] bytes) throws VerifyCodeSignException { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + // after put, rewind is mandatory before get + bf.rewind(); + int inTreeType = bf.getInt(); + if (MERKLE_TREE_INLINED != inTreeType) { + throw new VerifyCodeSignException("Invalid merkle tree type of ElfSignBlock"); + } + int inTreeLength = bf.getInt(); + byte[] treeWithPadding = new byte[inTreeLength]; + bf.get(treeWithPadding); + int inFsdType = bf.getInt(); + if (FsVerityDescriptor.FS_VERITY_DESCRIPTOR_TYPE != inFsdType) { + throw new VerifyCodeSignException("Invalid fs-verify descriptor type of ElfSignBlock"); + } + int inFsdLength = bf.getInt(); + if (bytes.length != Integer.BYTES * 2 + inTreeLength + Integer.BYTES * 2 + inFsdLength) { + throw new VerifyCodeSignException("Invalid fs-verify descriptor with signature length of ElfSignBlock"); + } + byte[] fsdArray = new byte[FsVerityDescriptor.DESCRIPTOR_SIZE]; + FsVerityDescriptor fsd = FsVerityDescriptor.fromByteArray(fsdArray); + if (inFsdLength != fsd.getSignSize() + FsVerityDescriptor.DESCRIPTOR_SIZE) { + throw new VerifyCodeSignException("Invalid sign size of ElfSignBlock"); + } + byte[] inSignature = new byte[inFsdLength - FsVerityDescriptor.DESCRIPTOR_SIZE]; + bf.get(inSignature); + FsVerityDescriptorWithSign fsVerityDescriptorWithSign = new FsVerityDescriptorWithSign(inFsdType, inFsdLength, + fsd, inSignature); + return new ElfSignBlock(inTreeType, inTreeLength, treeWithPadding, fsVerityDescriptorWithSign); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/Extension.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/Extension.java new file mode 100644 index 0000000000000000000000000000000000000000..2d23c548812465c6419fea07d40299268c9121cb --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/Extension.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Extension is an optional field in relative to SignInfo. + * It is the base class for all types of extensions, i.e. MerkleTreeExtension. + *

+ * Structure: + * u32 type: Indicates the type of extension + *

+ * u32 size: byte size of extension data + * + * @since 2023/09/08 + */ +public class Extension { + /** + * Byte size of Extension base class. + */ + public static final int EXTENSION_HEADER_SIZE = 8; + + private final int type; + + private final int size; + + public Extension(int type, int size) { + this.type = type; + this.size = size; + } + + public int size() { + return EXTENSION_HEADER_SIZE; + } + + public boolean isType(int type) { + return this.type == type; + } + + /** + * Converts Extension to a newly created byte array + * + * @return Byte array representation of Extension + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(EXTENSION_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(this.type); + bf.putInt(this.size); + return bf.array(); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, "Extension: type[%d], size[%d]", this.type, this.size); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/FsVerityInfoSegment.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/FsVerityInfoSegment.java new file mode 100644 index 0000000000000000000000000000000000000000..117431cf1dbb769c4f3757d3f0c955824e574cfd --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/FsVerityInfoSegment.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptor; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Fs-verity info segment contains information of fs-verity protection + * More information of fs-verity can be found here + *

+ * Structure + *

+ * 1) u32 magic: magic number + *

+ * 2) u8 version: fs-verity version + *

+ * 3) u8 hashAlgorithm: hash algorithm to use for the Merkle tree + *

+ * 4) u8 log2BlockSize: log2 of size of data and tree blocks + *

+ * 5) u8[] reserved: for reservation + * + * @since 2023/09/08 + */ +public class FsVerityInfoSegment { + /** + * fs-verity info segment size in bytes + */ + public static final int FS_VERITY_INFO_SEGMENT_SIZE = 64; + + // lower 4 bytes of the MD5 result of string "fs-verity info segment" (1E38 31AB) + private static final int MAGIC = (0x1E38 << 16) + (0x31AB); + + private static final int RESERVED_BYTE_ARRAY_LENGTH = 57; + + private int magic = MAGIC; + + private byte hashAlgorithm; + + private byte version; + + private byte log2BlockSize; + + private byte[] reserved = new byte[RESERVED_BYTE_ARRAY_LENGTH]; + + /** + * Default constructor + */ + public FsVerityInfoSegment() { + } + + public FsVerityInfoSegment(byte version, byte hashAlgorithm, byte log2BlockSize) { + this(MAGIC, version, hashAlgorithm, log2BlockSize, new byte[RESERVED_BYTE_ARRAY_LENGTH]); + } + + /** + * Constructor of FsVerityInfoSegment + * + * @param magic magic num + * @param version version of fs-verity + * @param hashAlgorithm hash algorithm to use for the Merkle tree + * @param log2BlockSize log2 of size of data and tree blocks + * @param reserved for reservation + */ + public FsVerityInfoSegment(int magic, byte version, byte hashAlgorithm, byte log2BlockSize, byte[] reserved) { + this.magic = magic; + this.version = version; + this.hashAlgorithm = hashAlgorithm; + this.log2BlockSize = log2BlockSize; + this.reserved = reserved; + } + + public int size() { + return FS_VERITY_INFO_SEGMENT_SIZE; + } + + /** + * Converts FsVerityInfoSegment to a newly created byte array + * + * @return Byte array representation of FsVerityInfoSegment + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(FS_VERITY_INFO_SEGMENT_SIZE).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(this.magic); + bf.put(version); + bf.put(hashAlgorithm); + bf.put(log2BlockSize); + bf.put(reserved); + return bf.array(); + } + + /** + * Init the FsVerityInfoSegment by a byte array + * + * @param bytes Byte array representation of a FsVerityInfoSegment object + * @return a newly created FsVerityInfoSegment object + * @throws VerifyCodeSignException parsing result invalid + */ + public static FsVerityInfoSegment fromByteArray(byte[] bytes) throws VerifyCodeSignException { + if (bytes.length != FS_VERITY_INFO_SEGMENT_SIZE) { + throw new VerifyCodeSignException("Invalid size of FsVerityInfoSegment"); + } + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + int inMagic = bf.getInt(); + if (inMagic != MAGIC) { + throw new VerifyCodeSignException("Invalid magic number of FsVerityInfoSegment"); + } + byte inVersion = bf.get(); + if (inVersion != FsVerityDescriptor.VERSION) { + throw new VerifyCodeSignException("Invalid version of FsVerityInfoSegment"); + } + byte inHashAlgorithm = bf.get(); + if (inHashAlgorithm != FsVerityGenerator.getFsVerityHashAlgorithm()) { + throw new VerifyCodeSignException("Invalid hashAlgorithm of FsVerityInfoSegment"); + } + byte inLog2BlockSize = bf.get(); + if (inLog2BlockSize != FsVerityGenerator.getLog2BlockSize()) { + throw new VerifyCodeSignException("Invalid log2BlockSize of FsVerityInfoSegment"); + } + byte[] inReservedBytes = new byte[RESERVED_BYTE_ARRAY_LENGTH]; + bf.get(inReservedBytes); + return new FsVerityInfoSegment(inMagic, inVersion, inHashAlgorithm, inLog2BlockSize, inReservedBytes); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, "FsVerityInfoSeg: magic[%d], version[%d], hashAlg[%d], log2BlockSize[%d]", + this.magic, this.version, this.hashAlgorithm, this.log2BlockSize); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/HapInfoSegment.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/HapInfoSegment.java new file mode 100644 index 0000000000000000000000000000000000000000..0a25ba2532b931737894f1372ca2e4643c2bbf15 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/HapInfoSegment.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Hap info segment + *

+ * Structure + *

+ * 1) u32 magic: magic number + *

+ * 2) SignInfo hapSignInfo: data struct of sign info, refer to SignInfo.java + * + * @since 2023/09/08 + */ +public class HapInfoSegment { + private static final int MAGIC_NUM_BYTES = 4; + + /** + * lower 4 bytes of the MD5 result of string "hap info segment" (C1B5 CC66) + */ + private static final int MAGIC_NUM = (0xC1B5 << 16) + 0xCC66; + + private int magic = MAGIC_NUM; + + private SignInfo hapSignInfo; + + /** + * Default constructor of HapInfoSegment + */ + public HapInfoSegment() { + this(MAGIC_NUM, new SignInfo(0, 0, 0, null, null)); + } + + /** + * Default constructor of HapInfoSegment + * + * @param magic magic number + * @param hapSignInfo hap sign info + */ + public HapInfoSegment(int magic, SignInfo hapSignInfo) { + this.magic = magic; + this.hapSignInfo = hapSignInfo; + } + + public void setSignInfo(SignInfo signInfo) { + this.hapSignInfo = signInfo; + } + + public SignInfo getSignInfo() { + return hapSignInfo; + } + + /** + * Returns byte size of HapInfoSegment + * + * @return byte size of HapInfoSegment + */ + public int size() { + return MAGIC_NUM_BYTES + hapSignInfo.size(); + } + + /** + * Converts HapInfoSegment to a newly created byte array + * + * @return Byte array representation of HapInfoSegment + */ + public byte[] toByteArray() { + byte[] hapSignInfoByteArray = this.hapSignInfo.toByteArray(); + // For now, only hap info segment has a merkle tree extension. So info segment + // has none extension. + ByteBuffer bf = ByteBuffer.allocate(MAGIC_NUM_BYTES + hapSignInfoByteArray.length) + .order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(magic); + bf.put(hapSignInfoByteArray); + return bf.array(); + } + + /** + * Init the HapInfoSegment by a byte array + * + * @param bytes Byte array representation of a HapInfoSegment object + * @return a newly created HapInfoSegment object + * @throws VerifyCodeSignException parsing result invalid + */ + public static HapInfoSegment fromByteArray(byte[] bytes) throws VerifyCodeSignException { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + int inMagic = bf.getInt(); + if (inMagic != MAGIC_NUM) { + throw new VerifyCodeSignException("Invalid magic number of HapInfoSegment"); + } + byte[] hapSignInfoByteArray = new byte[bytes.length - MAGIC_NUM_BYTES]; + bf.get(hapSignInfoByteArray); + SignInfo inHapSignInfo = SignInfo.fromByteArray(hapSignInfoByteArray); + if (inHapSignInfo.getDataSize() % CodeSignBlock.PAGE_SIZE_4K != 0) { + throw new VerifyCodeSignException( + String.format(Locale.ROOT, "Invalid dataSize number of HapInfoSegment, not a multiple of 4096: %d", + inHapSignInfo.getDataSize())); + } + if (inHapSignInfo.getExtensionNum() != SignInfo.MAX_EXTENSION_NUM) { + throw new VerifyCodeSignException("Invalid extensionNum of HapInfoSegment"); + } + if (inHapSignInfo.getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED) == null) { + throw new VerifyCodeSignException("No merkle tree extension is found in HapInfoSegment"); + } + return new HapInfoSegment(inMagic, inHapSignInfo); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, "HapInfoSegment: magic[%d], signInfo[%s]", this.magic, + this.hapSignInfo.toString()); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/MerkleTreeExtension.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/MerkleTreeExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..303d92edf13effedc56dec7605e4b2c3bd5aa908 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/MerkleTreeExtension.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Locale; + +/** + * Merkle tree extension is a type of Extension to store a merkle tree's information, i.e. size and root hash, ect. + *

+ * structure + *

+ * 1) u32 type + *

+ * 2) u64 merkleTreeSize: the size of merkle tree + *

+ * 3) u64 merkleTreeOffset: offset of the merkle tree by the start of the file. + *

+ * 4) u8[64] rootHash: merkle tree root hash + * + * @since 2023/09/08 + */ +public class MerkleTreeExtension extends Extension { + /** + * Type of MerkleTreeExtension + */ + public static final int MERKLE_TREE_INLINED = 0x1; + + /** + * Byte size of MerkleTreeExtension including merkleTreeSize, offset and root hash. + */ + public static final int MERKLE_TREE_EXTENSION_DATA_SIZE = 80; + + private static final int ROOT_HASH_SIZE = 64; + + private final long merkleTreeSize; + + private long merkleTreeOffset; + + private byte[] rootHash; + + /** + * Constructor for MerkleTreeExtension + * + * @param merkleTreeSize Byte array representation of merkle tree + * @param merkleTreeOffset merkle tree offset based on file start + * @param rootHash Root hash of the merkle tree + */ + public MerkleTreeExtension(long merkleTreeSize, long merkleTreeOffset, byte[] rootHash) { + super(MERKLE_TREE_INLINED, MERKLE_TREE_EXTENSION_DATA_SIZE); + this.merkleTreeSize = merkleTreeSize; + this.merkleTreeOffset = merkleTreeOffset; + if (rootHash == null) { + this.rootHash = new byte[ROOT_HASH_SIZE]; + } else { + this.rootHash = Arrays.copyOf(rootHash, ROOT_HASH_SIZE); + } + } + + @Override + public int size() { + return Extension.EXTENSION_HEADER_SIZE + MERKLE_TREE_EXTENSION_DATA_SIZE; + } + + public long getMerkleTreeSize() { + return merkleTreeSize; + } + + public long getMerkleTreeOffset() { + return merkleTreeOffset; + } + + public void setMerkleTreeOffset(long offset) { + this.merkleTreeOffset = offset; + } + + /** + * Converts MerkleTreeExtension to a newly created byte array + * + * @return Byte array representation of MerkleTreeExtension + */ + @Override + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(Extension.EXTENSION_HEADER_SIZE + MERKLE_TREE_EXTENSION_DATA_SIZE) + .order(ByteOrder.LITTLE_ENDIAN); + bf.put(super.toByteArray()); + bf.putLong(this.merkleTreeSize); + bf.putLong(this.merkleTreeOffset); + bf.put(this.rootHash); + return bf.array(); + } + + /** + * Init the MerkleTreeExtension by a byte array + * + * @param bytes Byte array representation of a MerkleTreeExtension object + * @return a newly created MerkleTreeExtension object + * @throws VerifyCodeSignException parsing result invalid + */ + public static MerkleTreeExtension fromByteArray(byte[] bytes) throws VerifyCodeSignException { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + long inMerkleTreeSize = bf.getLong(); + if (inMerkleTreeSize % CodeSignBlock.PAGE_SIZE_4K != 0) { + throw new VerifyCodeSignException("merkleTreeSize is not a multiple of 4096"); + } + long inMerkleTreeOffset = bf.getLong(); + if (inMerkleTreeOffset % CodeSignBlock.PAGE_SIZE_4K != 0) { + throw new VerifyCodeSignException("merkleTreeOffset is not a aligned to 4096"); + } + byte[] inRootHash = new byte[ROOT_HASH_SIZE]; + bf.get(inRootHash); + return new MerkleTreeExtension(inMerkleTreeSize, inMerkleTreeOffset, inRootHash); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + @Override + public String toString() { + return String.format(Locale.ROOT, "MerkleTreeExtension: merkleTreeSize[%d], merkleTreeOffset[%d]," + + " rootHash[%s]", this.merkleTreeSize, this.merkleTreeOffset, Arrays.toString(this.rootHash)); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/NativeLibInfoSegment.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/NativeLibInfoSegment.java new file mode 100644 index 0000000000000000000000000000000000000000..e7bcfc49d5db540dc34409db85df2d714e71584a --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/NativeLibInfoSegment.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; +import com.ohos.hapsigntool.hap.entity.Pair; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * SoInfoSegment consists of a header part: + *

+ * u32 magic: magic number + *

+ * u32 length: byte size of SoInfoSegment + *

+ * u32 section num: the amount of file being signed + *

+ * Followed by an area containing the offset and size of each file being signed with its signed info: + *

+ * u32 file name offset: position of file name based on the start of SoInfoSegment + *

+ * u32 file name size : byte size of file name string + *

+ * u32 sign info offset : position of signed info based on the start of SoInfoSegment + *

+ * u32 sign size: byte size of signed info + *

+ * Ends with the file name and signed info content: + *

+ * file name List : file name of each signed file + *

+ * sign info List : signed info of each file + *

+ * + * @since 2023/09/08 + */ +public class NativeLibInfoSegment { + private static final int MAGIC_LENGTH_SECNUM_BYTES = 12; + + private static final int SIGNED_FILE_POS_SIZE = 16; + + // lower 4 bytes of the MD5 result of string "native lib info segment" (0ED2 E720) + private static final int MAGIC_NUM = (0x0ED2 << 16) + 0xE720; + + private static final int ALIGNMENT_FOR_SIGNINFO = 4; + + private int magic; + + private int segmentSize; + + private int sectionNum; + + private List> soInfoList = new ArrayList<>(); + + private List signedFilePosList; + + private List fileNameList; + + private List signInfoList; + + private byte[] zeroPadding; + + private int fileNameListBlockSize; + + private int signInfoListBlockSize; + + /** + * Constructor for SoInfoSegment + * + * @param builder Builder + */ + private NativeLibInfoSegment(Builder builder) { + this.magic = builder.magic; + this.segmentSize = builder.segmentSize; + this.sectionNum = builder.sectionNum; + this.signedFilePosList = builder.signedFilePosList; + this.fileNameList = builder.fileNameList; + this.signInfoList = builder.signInfoList; + this.zeroPadding = builder.zeroPadding; + } + + /** + * set soInfoList, generate fileNameList and soInfoList + * + * @param soInfoList list of file and its signed info + */ + public void setSoInfoList(List> soInfoList) { + this.soInfoList = soInfoList; + // Once map is set, update length, sectionNum as well + this.sectionNum = soInfoList.size(); + // generate file name list and sign info list + generateList(); + } + + public int getSectionNum() { + return sectionNum; + } + + public List getFileNameList() { + return fileNameList; + } + + public List getSignInfoList() { + return signInfoList; + } + + // generate List based on current so + private void generateList() { + // empty all before generate list + this.fileNameList.clear(); + this.signInfoList.clear(); + this.signedFilePosList.clear(); + int fileNameOffset = 0; + int signInfoOffset = 0; + for (Pair soInfo : soInfoList) { + String fileName = soInfo.getFirst(); + SignInfo signInfo = soInfo.getSecond(); + int fileNameSizeInBytes = fileName.getBytes(StandardCharsets.UTF_8).length; + int signInfoSizeInBytes = signInfo.toByteArray().length; + this.fileNameList.add(fileName); + this.signInfoList.add(signInfo); + this.signedFilePosList.add( + new SignedFilePos(fileNameOffset, fileNameSizeInBytes, signInfoOffset, signInfoSizeInBytes)); + // increase fileNameOffset and signInfoOffset + fileNameOffset += fileNameSizeInBytes; + signInfoOffset += signInfoSizeInBytes; + } + this.fileNameListBlockSize = fileNameOffset; + this.signInfoListBlockSize = signInfoOffset; + // alignment for signInfo + this.zeroPadding = new byte[(ALIGNMENT_FOR_SIGNINFO - this.fileNameListBlockSize % ALIGNMENT_FOR_SIGNINFO) + % ALIGNMENT_FOR_SIGNINFO]; + // after fileNameList and signInfoList is generated, update segment size + this.segmentSize = this.size(); + // adjust file name and sign info offset base on segment start + int fileNameOffsetBase = MAGIC_LENGTH_SECNUM_BYTES + signedFilePosList.size() * SIGNED_FILE_POS_SIZE; + int signInfoOffsetBase = fileNameOffsetBase + this.fileNameListBlockSize; + for (SignedFilePos pos : this.signedFilePosList) { + pos.increaseFileNameOffset(fileNameOffsetBase); + pos.increaseSignInfoOffset(signInfoOffsetBase + this.zeroPadding.length); + } + } + + /** + * Returns byte size of SoInfoSegment + * + * @return byte size of SoInfoSegment + */ + public int size() { + int blockSize = MAGIC_LENGTH_SECNUM_BYTES; + blockSize += signedFilePosList.size() * SIGNED_FILE_POS_SIZE; + blockSize += this.fileNameListBlockSize + this.zeroPadding.length + this.signInfoListBlockSize; + return blockSize; + } + + /** + * Converts SoInfoSegment to a newly created byte array + * + * @return Byte array representation of SoInfoSegment + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(this.size()).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(magic); + bf.putInt(segmentSize); + bf.putInt(sectionNum); + for (SignedFilePos offsetAndSize : this.signedFilePosList) { + bf.putInt(offsetAndSize.getFileNameOffset()); + bf.putInt(offsetAndSize.getFileNameSize()); + bf.putInt(offsetAndSize.getSignInfoOffset()); + bf.putInt(offsetAndSize.getSignInfoSize()); + } + for (String fileName : fileNameList) { + bf.put(fileName.getBytes(StandardCharsets.UTF_8)); + } + bf.put(this.zeroPadding); + for (SignInfo signInfo : signInfoList) { + bf.put(signInfo.toByteArray()); + } + return bf.array(); + } + + /** + * Init the SoInfoSegment by a byte array + * + * @param bytes Byte array representation of a SoInfoSegment object + * @return a newly created NativeLibInfoSegment object + * @throws VerifyCodeSignException parsing result invalid + */ + public static NativeLibInfoSegment fromByteArray(byte[] bytes) throws VerifyCodeSignException { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + int inMagic = bf.getInt(); + if (inMagic != MAGIC_NUM) { + throw new VerifyCodeSignException("Invalid magic number of NativeLibInfoSegment"); + } + int inSegmentSize = bf.getInt(); + if (inSegmentSize < 0) { + throw new VerifyCodeSignException("Invalid segmentSize of NativeLibInfoSegment"); + } + int inSectionNum = bf.getInt(); + if (inSectionNum < 0) { + throw new VerifyCodeSignException("Invalid sectionNum of NativeLibInfoSegment"); + } + List inSignedFilePosList = new ArrayList<>(); + for (int i = 0; i < inSectionNum; i++) { + byte[] entry = new byte[SIGNED_FILE_POS_SIZE]; + bf.get(entry); + inSignedFilePosList.add(SignedFilePos.fromByteArray(entry)); + } + // parse file name list + List inFileNameList = new ArrayList<>(); + int fileNameListSize = 0; + for (SignedFilePos pos : inSignedFilePosList) { + byte[] fileNameBuffer = new byte[pos.getFileNameSize()]; + fileNameListSize += pos.getFileNameSize(); + bf.get(fileNameBuffer); + inFileNameList.add(new String(fileNameBuffer, StandardCharsets.UTF_8)); + } + // parse zeroPadding + byte[] inZeroPadding = new byte[(ALIGNMENT_FOR_SIGNINFO - fileNameListSize % ALIGNMENT_FOR_SIGNINFO) + % ALIGNMENT_FOR_SIGNINFO]; + bf.get(inZeroPadding); + // parse sign info list + List inSignInfoList = new ArrayList<>(); + for (SignedFilePos pos : inSignedFilePosList) { + if (pos.getSignInfoOffset() % ALIGNMENT_FOR_SIGNINFO != 0) { + throw new VerifyCodeSignException("SignInfo not aligned in NativeLibInfoSegment"); + } + byte[] signInfoBuffer = new byte[pos.getSignInfoSize()]; + bf.get(signInfoBuffer); + inSignInfoList.add(SignInfo.fromByteArray(signInfoBuffer)); + } + return new Builder().setMagic(inMagic).setSegmentSize(inSegmentSize).setSectionNum(inSectionNum) + .setSignedFilePosList(inSignedFilePosList).setFileNameList(inFileNameList) + .setSignInfoList(inSignInfoList).setZeroPadding(inZeroPadding).build(); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, + "SoInfoSegment: magic[%d], length[%d], secNum[%d], signedFileEntryList[%s], fileNameList[%s], " + + "zeroPadding[%s], signInfoList[%s]", this.magic, this.segmentSize, this.sectionNum, + Arrays.toString(this.signedFilePosList.toArray()), Arrays.toString(this.fileNameList.toArray()), + Arrays.toString(this.zeroPadding), Arrays.toString(this.signInfoList.toArray())); + } + + /** + * Builder of NativeLibInfoSegment class + */ + public static class Builder { + private int magic = MAGIC_NUM; + + private int segmentSize; + + private int sectionNum; + + private List signedFilePosList = new ArrayList<>(); + + private List fileNameList = new ArrayList<>(); + + private List signInfoList = new ArrayList<>(); + + private byte[] zeroPadding = new byte[0]; + + public Builder setMagic(int magic) { + this.magic = magic; + return this; + } + + public Builder setSegmentSize(int segmentSize) { + this.segmentSize = segmentSize; + return this; + } + + public Builder setSectionNum(int sectionNum) { + this.sectionNum = sectionNum; + return this; + } + + public Builder setSignedFilePosList(List signedFilePosList) { + this.signedFilePosList = signedFilePosList; + return this; + } + + public Builder setFileNameList(List fileNameList) { + this.fileNameList = fileNameList; + return this; + } + + public Builder setSignInfoList(List signInfoList) { + this.signInfoList = signInfoList; + return this; + } + + public Builder setZeroPadding(byte[] zeroPadding) { + this.zeroPadding = zeroPadding; + return this; + } + + /** + * Create a NativeLibInfoSegment object + * + * @return a NativeLibInfoSegment object + */ + public NativeLibInfoSegment build() { + return new NativeLibInfoSegment(this); + } + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SegmentHeader.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SegmentHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..975dda21553f8f8687784640839fc365852615de --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SegmentHeader.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * segment header has three field: + *

+ * u32 type: indicates the type of segment: fs-verity/so/hap info segment + *

+ * u32 segment offset: the segment position based on the start of code sign block + *

+ * u32 segment size: byte size of the segment + * + * @since 2023/09/08 + */ +public class SegmentHeader { + /** + * Byte size of SegmentHeader + */ + public static final int SEGMENT_HEADER_LENGTH = 12; + + /** + * Fs-verity segment type + */ + public static final int CSB_FSVERITY_INFO_SEG = 0x1; + + /** + * Hap info segment type + */ + public static final int CSB_HAP_META_SEG = 0x2; + + /** + * So info segment type + */ + public static final int CSB_NATIVE_LIB_INFO_SEG = 0x3; + + private final int type; + + private int segmentOffset; + + private final int segmentSize; + + /** + * Constructor for SegmentHeader + * + * @param type segment type + * @param segmentSize byte size of the segment + */ + public SegmentHeader(int type, int segmentSize) { + this(type, 0, segmentSize); + } + + /** + * Constructor for SegmentHeader + * + * @param type segment type + * @param segmentOffset segment offset based on the start of code sign block + * @param segmentSize byte size of segment + */ + public SegmentHeader(int type, int segmentOffset, int segmentSize) { + this.type = type; + this.segmentOffset = segmentOffset; + this.segmentSize = segmentSize; + } + + public int getType() { + return type; + } + + public void setSegmentOffset(int offset) { + this.segmentOffset = offset; + } + + public int getSegmentOffset() { + return segmentOffset; + } + + public int getSegmentSize() { + return segmentSize; + } + + /** + * Converts SegmentHeader to a newly created byte array + * + * @return Byte array representation of SegmentHeader + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(SEGMENT_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(type); + bf.putInt(segmentOffset); + bf.putInt(segmentSize); + return bf.array(); + } + + /** + * Init the SegmentHeader by a byte array + * + * @param bytes Byte array representation of a SegmentHeader object + * @return a newly created SegmentHeader object + * @throws VerifyCodeSignException parsing result invalid + */ + public static SegmentHeader fromByteArray(byte[] bytes) throws VerifyCodeSignException { + if (bytes.length != SEGMENT_HEADER_LENGTH) { + throw new VerifyCodeSignException("Invalid size of SegmentHeader"); + } + ByteBuffer bf = ByteBuffer.allocate(SEGMENT_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + int inType = bf.getInt(); + if ((inType != CSB_FSVERITY_INFO_SEG) && (inType != CSB_HAP_META_SEG) && (inType != CSB_NATIVE_LIB_INFO_SEG)) { + throw new VerifyCodeSignException("Invalid type of SegmentHeader"); + } + int inSegmentOffset = bf.getInt(); + // segment offset is always larger than the size of CodeSignBlockHeader + if (inSegmentOffset < CodeSignBlockHeader.size()) { + throw new VerifyCodeSignException("Invalid segmentOffset of SegmentHeader"); + } + int inSegmentSize = bf.getInt(); + if (inSegmentSize < 0) { + throw new VerifyCodeSignException("Invalid segmentSize of SegmentHeader"); + } + if ((inType == CSB_FSVERITY_INFO_SEG) && (inSegmentSize != FsVerityInfoSegment.FS_VERITY_INFO_SEGMENT_SIZE)) { + throw new VerifyCodeSignException("Invalid segmentSize of fs-verity SegmentHeader"); + } + return new SegmentHeader(inType, inSegmentOffset, inSegmentSize); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, "Segment Header: type=%d, seg_offset = %d, seg_size = %d", this.type, + this.segmentOffset, this.segmentSize); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SignInfo.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SignInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..e2d538c4f3dc2cd6d05518caf9240c7be54b9d50 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SignInfo.java @@ -0,0 +1,452 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Sign info represents information after signing a file, including signature, merkle tree. + * Structure: + *

+ * 1) u32 saltSize: byte size of salt + *

+ * 2) u32 sigSize: byte size of signature + *

+ * 3) u32 flags: reserved flags + *

+ * 4) u64 dataSize: byte size of data being signed + *

+ * 5) u8[32] salt: salt used in signing + *

+ * 6) u32 extensionNum: number of extension + *

+ * 7) u32 extensionOffset + *

+ * 8) u8[] signature: signature of the data + *

+ * MerkleTree is represented as an extension of the sign info. + * Its structure is defined in MerkleTreeExtension.java + * + * @since 2023/09/08 + */ +public class SignInfo { + /** + * merkle tree extension is included in sign info + */ + public static final int FLAG_MERKLE_TREE_INCLUDED = 0x1; + + /** + * maximum of extension number + */ + public static final int MAX_EXTENSION_NUM = 1; + + /** + * sign info structure without signature in bytes, refer to toByteArray() method + */ + private static final int SIGN_INFO_SIZE_WITHOUT_SIGNATURE = 60; + + private static final int SALT_BUFFER_LENGTH = 32; + + private static final int SIGNATURE_ALIGNMENT = 4; + + private int saltSize; + + private int sigSize; + + private int flags; + + private long dataSize; + + private byte[] salt; + + private int extensionNum; + + private int extensionOffset; + + private byte[] signature; + + private byte[] zeroPadding; + + // temporary, use list instead + private List extensionList = new ArrayList<>(); + + /** + * Constructor for SignInfo + * + * @param saltSize byte size of salt + * @param flags reserved flags + * @param dataSize byte size of data being signed + * @param salt salt in byte array representation + * @param sig signature after signing the data in byte array representation + */ + public SignInfo(int saltSize, int flags, long dataSize, byte[] salt, byte[] sig) { + this.saltSize = saltSize; + this.flags = flags; + this.dataSize = dataSize; + if (salt == null) { + this.salt = new byte[SALT_BUFFER_LENGTH]; + } else { + this.salt = salt; + } + this.signature = sig; + this.sigSize = sig == null ? 0 : sig.length; + // align for extension after signature + this.zeroPadding = new byte[(SIGNATURE_ALIGNMENT - (this.sigSize % SIGNATURE_ALIGNMENT)) % SIGNATURE_ALIGNMENT]; + } + + /** + * Constructor by a SignInfoBuilder + * + * @param builder SignInfoBuilder + */ + private SignInfo(SignInfoBuilder builder) { + this.saltSize = builder.saltSize; + this.sigSize = builder.sigSize; + this.flags = builder.flags; + this.dataSize = builder.dataSize; + this.salt = builder.salt; + this.extensionNum = builder.extensionNum; + this.extensionOffset = builder.extensionOffset; + this.signature = builder.signature; + this.zeroPadding = builder.zeroPadding; + this.extensionList = builder.extensionList; + } + + /** + * Add one Extension into SignInfo Object + * + * @param extension Extension object + */ + public void addExtension(Extension extension) { + this.extensionOffset = this.size(); + this.extensionList.add(extension); + this.extensionNum = this.extensionList.size(); + } + + /** + * Get Extension from SignInfo based on extension type + * + * @param type extension type + * @return Extension object + */ + public Extension getExtensionByType(int type) { + for (Extension ext : this.extensionList) { + if (ext.isType(type)) { + return ext; + } + } + return null; + } + + /** + * Returns extensionNum + * + * @return extensionNum + */ + public int getExtensionNum() { + return extensionNum; + } + + public byte[] getSignature() { + return signature; + } + + public long getDataSize() { + return dataSize; + } + + /** + * Returns byte size of SignInfo object + * + * @return byte size of SignInfo object + */ + public int size() { + int blockSize = SIGN_INFO_SIZE_WITHOUT_SIGNATURE + this.signature.length + this.zeroPadding.length; + for (Extension ext : this.extensionList) { + blockSize += ext.size(); + } + return blockSize; + } + + /** + * Converts SignInfo to a newly created byte array + * + * @return Byte array representation of SignInfo + */ + public byte[] toByteArray() { + ByteBuffer bf = ByteBuffer.allocate(this.size()).order(ByteOrder.LITTLE_ENDIAN); + bf.putInt(this.saltSize); + bf.putInt(this.sigSize); + bf.putInt(this.flags); + bf.putLong(this.dataSize); + bf.put(this.salt); + bf.putInt(this.extensionNum); + bf.putInt(this.extensionOffset); + bf.put(this.signature); + bf.put(this.zeroPadding); + // put extension + for (Extension ext : this.extensionList) { + bf.put(ext.toByteArray()); + } + return bf.array(); + } + + /** + * Init the SignInfo by a byte array + * + * @param bytes Byte array representation of a SignInfo object + * @return a newly created SignInfo object + * @throws VerifyCodeSignException parsing result invalid + */ + public static SignInfo fromByteArray(byte[] bytes) throws VerifyCodeSignException { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + int inSaltSize = bf.getInt(); + if (inSaltSize < 0) { + throw new VerifyCodeSignException("Invalid saltSize of SignInfo"); + } + int inSigSize = bf.getInt(); + if (inSigSize < 0) { + throw new VerifyCodeSignException("Invalid sigSize of SignInfo"); + } + int inFlags = bf.getInt(); + if (inFlags != 0 && inFlags != FLAG_MERKLE_TREE_INCLUDED) { + throw new VerifyCodeSignException("Invalid flags of SignInfo"); + } + long inDataSize = bf.getLong(); + if (inDataSize < 0) { + throw new VerifyCodeSignException("Invalid dataSize of SignInfo"); + } + byte[] inSalt = new byte[SALT_BUFFER_LENGTH]; + bf.get(inSalt); + int inExtensionNum = bf.getInt(); + if (inExtensionNum < 0 || inExtensionNum > MAX_EXTENSION_NUM) { + throw new VerifyCodeSignException("Invalid extensionNum of SignInfo"); + } + int inExtensionOffset = bf.getInt(); + if (inExtensionOffset < 0 || inExtensionOffset % 4 != 0) { + throw new VerifyCodeSignException("Invalid extensionOffset of SignInfo"); + } + byte[] inSignature = new byte[inSigSize]; + bf.get(inSignature); + byte[] inZeroPadding = new byte[(SIGNATURE_ALIGNMENT - (inSigSize % SIGNATURE_ALIGNMENT)) + % SIGNATURE_ALIGNMENT]; + bf.get(inZeroPadding); + // parse merkle tree extension + List inExtensionList = parseMerkleTreeExtension(bf, inExtensionNum); + return new SignInfoBuilder().setSaltSize(inSaltSize) + .setSigSize(inSigSize) + .setFlags(inFlags) + .setDataSize(inDataSize) + .setSalt(inSalt) + .setExtensionNum(inExtensionNum) + .setExtensionOffset(inExtensionOffset) + .setSignature(inSignature) + .setZeroPadding(inZeroPadding) + .setExtensionList(inExtensionList) + .build(); + } + + private static List parseMerkleTreeExtension(ByteBuffer bf, int inExtensionNum) + throws VerifyCodeSignException { + List inExtensionList = new ArrayList<>(); + if (inExtensionNum == 1) { + // parse merkle tree extension + int extensionType = bf.getInt(); + if (extensionType != MerkleTreeExtension.MERKLE_TREE_INLINED) { + throw new VerifyCodeSignException("Invalid extensionType of SignInfo"); + } + int extensionSize = bf.getInt(); + if (extensionSize != MerkleTreeExtension.MERKLE_TREE_EXTENSION_DATA_SIZE) { + throw new VerifyCodeSignException("Invalid extensionSize of SignInfo"); + } + byte[] merkleTreeExtension = new byte[MerkleTreeExtension.MERKLE_TREE_EXTENSION_DATA_SIZE]; + bf.get(merkleTreeExtension); + inExtensionList.add(MerkleTreeExtension.fromByteArray(merkleTreeExtension)); + } + return inExtensionList; + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + String str = String.format(Locale.ROOT, "SignInfo: saltSize[%d], sigSize[%d]," + + "flags[%d], dataSize[%d], salt[%s], zeroPad[%s], extNum[%d], extOffset[%d]", + this.saltSize, this.sigSize, this.flags, this.dataSize, Arrays.toString(this.salt), + Arrays.toString(this.zeroPadding), this.extensionNum, this.extensionOffset); + if (this.getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED) != null) { + str += String.format(Locale.ROOT, "SignInfo.merkleTreeExtension[%s]", + this.getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED).toString()); + } + return str; + } + + /** + * Builder of SignInfo object + */ + public static class SignInfoBuilder { + private int saltSize; + + private int sigSize; + + private int flags; + + private long dataSize; + + private byte[] salt; + + private int extensionNum; + + private int extensionOffset; + + private byte[] signature; + + private byte[] zeroPadding; + + // temporary, use list instead + private List extensionList = new ArrayList<>(); + + /** + * set saltSize + * + * @param saltSize saltSize + * @return SignInfoBuilder + */ + public SignInfoBuilder setSaltSize(int saltSize) { + this.saltSize = saltSize; + return this; + } + + /** + * set sigSize + * + * @param sigSize sigSize + * @return SignInfoBuilder + */ + public SignInfoBuilder setSigSize(int sigSize) { + this.sigSize = sigSize; + return this; + } + + /** + * set flags + * + * @param flags flags + * @return SignInfoBuilder + */ + public SignInfoBuilder setFlags(int flags) { + this.flags = flags; + return this; + } + + /** + * set dataSize + * + * @param dataSize dataSize + * @return SignInfoBuilder + */ + public SignInfoBuilder setDataSize(long dataSize) { + this.dataSize = dataSize; + return this; + } + + /** + * set salt + * + * @param salt salt + * @return SignInfoBuilder + */ + public SignInfoBuilder setSalt(byte[] salt) { + this.salt = salt; + return this; + } + + /** + * set extensionNum + * + * @param extensionNum extensionNum + * @return SignInfoBuilder + */ + public SignInfoBuilder setExtensionNum(int extensionNum) { + this.extensionNum = extensionNum; + return this; + } + + /** + * set extensionOffset + * + * @param extensionOffset extensionOffset + * @return SignInfoBuilder + */ + public SignInfoBuilder setExtensionOffset(int extensionOffset) { + this.extensionOffset = extensionOffset; + return this; + } + + /** + * set signature + * + * @param signature signature + * @return SignInfoBuilder + */ + public SignInfoBuilder setSignature(byte[] signature) { + this.signature = signature; + return this; + } + + /** + * set zeroPadding + * + * @param zeroPadding zeroPadding + * @return SignInfoBuilder + */ + public SignInfoBuilder setZeroPadding(byte[] zeroPadding) { + this.zeroPadding = zeroPadding; + return this; + } + + /** + * set extensionList + * + * @param extensionList extensionList + * @return SignInfoBuilder + */ + public SignInfoBuilder setExtensionList(List extensionList) { + this.extensionList = extensionList; + return this; + } + + /** + * return a SignInfo object + * + * @return SignInfo object + */ + public SignInfo build() { + return new SignInfo(this); + } + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SignedFilePos.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SignedFilePos.java new file mode 100644 index 0000000000000000000000000000000000000000..6b2eb33593798e0390b476cbf13ea72e6a12ad39 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/datastructure/SignedFilePos.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.datastructure; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * Sign info of file + * + * @since 2023/09/08 + */ +public class SignedFilePos { + /** + * file name offset based on start of so info segment + */ + private int fileNameOffset; + + /** + * byte size of file + */ + private final int fileNameSize; + + /** + * sign info offset based on start of so info segment + */ + private int signInfoOffset; + + /** + * byte size of sign info + */ + private final int signInfoSize; + + /** + * Constructor for SignedFilePos + * + * @param fileNameOffset file name offset based on segment start + * @param fileNameSize byte size of file name string + * @param signInfoOffset sign info offset based on segment start + * @param signInfoSize byte size of sign info + */ + public SignedFilePos(int fileNameOffset, int fileNameSize, int signInfoOffset, int signInfoSize) { + this.fileNameOffset = fileNameOffset; + this.fileNameSize = fileNameSize; + this.signInfoOffset = signInfoOffset; + this.signInfoSize = signInfoSize; + } + + public int getFileNameOffset() { + return fileNameOffset; + } + + public int getFileNameSize() { + return fileNameSize; + } + + public int getSignInfoOffset() { + return signInfoOffset; + } + + public int getSignInfoSize() { + return signInfoSize; + } + + /** + * increase file name offset + * + * @param incOffset increase value + */ + public void increaseFileNameOffset(int incOffset) { + this.fileNameOffset += incOffset; + } + + /** + * increase sign info offset + * + * @param incOffset increase value + */ + public void increaseSignInfoOffset(int incOffset) { + this.signInfoOffset += incOffset; + } + + /** + * Constructor for SignedFilePos by byte array + * + * @param bytes Byte array representation of SignedFilePos + * @return a newly created SignedFilePos object + */ + public static SignedFilePos fromByteArray(byte[] bytes) { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + bf.rewind(); + int inFileNameOffset = bf.getInt(); + int inFileNameSize = bf.getInt(); + int inSignInfoOffset = bf.getInt(); + int inSignInfoSize = bf.getInt(); + return new SignedFilePos(inFileNameOffset, inFileNameSize, inSignInfoOffset, inSignInfoSize); + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, "SignedFilePos: fileNameOffset, Size[%d, %d], signInfoOffset, Size[%d, %d]", + this.fileNameOffset, this.fileNameSize, this.signInfoOffset, this.signInfoSize); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/CodeSignException.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/CodeSignException.java new file mode 100644 index 0000000000000000000000000000000000000000..8263bf508fa60ecf16d5b408adf3c2482000e6db --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/CodeSignException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.exception; + +/** + * CodeSign exception + * + * @since 2023/06/05 + */ +public class CodeSignException extends Exception { + private static final long serialVersionUID = -281871003709431259L; + + /** + * CodeSignException + * + * @param message msg + */ + public CodeSignException(String message) { + super(message); + } + + /** + * CodeSignException + * + * @param message msg + * @param cause cause + */ + public CodeSignException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/FsVerityDigestException.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/FsVerityDigestException.java new file mode 100644 index 0000000000000000000000000000000000000000..5ac1210bce1aaf1ef0a3cf8273dca12f0afa236a --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/FsVerityDigestException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.exception; + +/** + * Fail to compute FsVerity digest of a file + * + * @since 2023/06/05 + */ +public class FsVerityDigestException extends Exception { + private static final long serialVersionUID = 5788641970791287892L; + + public FsVerityDigestException(String message) { + super(message); + } + + public FsVerityDigestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/VerifyCodeSignException.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/VerifyCodeSignException.java new file mode 100644 index 0000000000000000000000000000000000000000..36d28623a55e49399ec3ab83ea2f78f1e0ca59dd --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/exception/VerifyCodeSignException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.exception; + +/** + * Exception occurs when the required parameters are missed + * + * @since 2023/06/05 + */ +public class VerifyCodeSignException extends Exception { + private static final long serialVersionUID = -8922730964374794468L; + + /** + * Exception occurs when the required parameters are not entered + * + * @param message msg + */ + public VerifyCodeSignException(String message) { + super(message); + } + + /** + * Exception occurs when the required parameters are not entered + * + * @param message msg + * @param cause cause + */ + public VerifyCodeSignException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDescriptor.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDescriptor.java new file mode 100644 index 0000000000000000000000000000000000000000..98cdd8964e9783ffa0d705c63f091e2aac93e801 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDescriptor.java @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Format of FsVerity descriptor + * uint8 version + * uint8 hashAlgorithm + * uint8 log2BlockSize + * uint8 saltSize + * uint32 signSize + * le64 dataSize + * uint8[64] rootHash + * uint8[32] salt + * uint32 flags + * uint8[4] 0 + * uint64 treeOffset + * uint8[127] 0 + * uint8 csVersion + * + * @since 2023/06/05 + */ +public class FsVerityDescriptor { + /** + * fs-verity version, must be 1 + */ + public static final byte VERSION = 1; + + /** + * page size in bytes + */ + public static final int PAGE_SIZE_4K = 4096; + + /** + * Indicating merkle tree offset is set in fs-verity descriptor + */ + public static final int FLAG_STORE_MERKLE_TREE_OFFSET = 0x1; + + /** + * Indicating fs-verity descriptor type + */ + public static final int FS_VERITY_DESCRIPTOR_TYPE = 0x1; + + /** + * code sign version + */ + public static final byte CODE_SIGN_VERSION = 0x1; + + /** + * FsVerity descriptor size + */ + public static final int DESCRIPTOR_SIZE = 256; + + /** + * root hash size + */ + public static final int ROOT_HASH_FILED_SIZE = 64; + + /** + * salt size + */ + public static final int SALT_SIZE = 32; + + /** + * reserved size + */ + public static final int RESERVED_SIZE_AFTER_FLAGS = 4; + + /** + * reserved size + */ + public static final int RESERVED_SIZE_AFTER_TREE_OFFSET = 127; + + private byte version; + + private long fileSize; + + private byte hashAlgorithm; + + private byte log2BlockSize; + + private byte saltSize; + + private int signSize; + + private byte[] salt; + + private byte[] rawRootHash; + + private int flags; + + private long merkleTreeOffset; + + private byte csVersion; + + private FsVerityDescriptor(Builder builder) { + this.version = builder.version; + this.fileSize = builder.fileSize; + this.hashAlgorithm = builder.hashAlgorithm; + this.log2BlockSize = builder.log2BlockSize; + this.saltSize = builder.saltSize; + this.signSize = builder.signSize; + this.salt = builder.salt; + this.rawRootHash = builder.rawRootHash; + this.flags = builder.flags; + this.merkleTreeOffset = builder.merkleTreeOffset; + this.csVersion = builder.csVersion; + } + + public long getFileSize() { + return fileSize; + } + + public long getMerkleTreeOffset() { + return merkleTreeOffset; + } + + public int getSignSize() { + return signSize; + } + + /** + * Init the FsVerityDescriptor by a byte array + * + * @param bytes Byte array representation of a FsVerityDescriptor object + * @return a newly created FsVerityDescriptor object + * @throws VerifyCodeSignException parse result invalid + */ + public static FsVerityDescriptor fromByteArray(byte[] bytes) throws VerifyCodeSignException { + ByteBuffer bf = ByteBuffer.allocate(bytes.length).order(ByteOrder.LITTLE_ENDIAN); + bf.put(bytes); + // after put, rewind is mandatory before get + bf.rewind(); + FsVerityDescriptor.Builder builder = new FsVerityDescriptor.Builder(); + byte inFsVersion = bf.get(); + if (FsVerityDescriptor.VERSION != inFsVersion) { + throw new VerifyCodeSignException("Invalid fs-verify descriptor version of ElfSignBlock"); + } + byte inFsHashAlgorithm = bf.get(); + byte inLog2BlockSize = bf.get(); + builder.setVersion(inFsVersion).setHashAlgorithm(inFsHashAlgorithm).setLog2BlockSize(inLog2BlockSize); + byte inSaltSize = bf.get(); + int inSignSize = bf.getInt(); + int inDataSize = bf.getInt(); + byte[] inRootHash = new byte[FsVerityDescriptor.ROOT_HASH_FILED_SIZE]; + bf.get(inRootHash); + builder.setSaltSize(inSaltSize).setSignSize(inSignSize).setFileSize(inDataSize).setRawRootHash(inRootHash); + byte[] inSalt = new byte[FsVerityDescriptor.SALT_SIZE]; + bf.get(inSalt); + int inFlags = bf.getInt(); + bf.getInt(); + long inTreeOffset = bf.getLong(); + if (inTreeOffset % PAGE_SIZE_4K != 0) { + throw new VerifyCodeSignException("Invalid merkle tree offset of ElfSignBlock"); + } + bf.get(new byte[FsVerityDescriptor.RESERVED_SIZE_AFTER_TREE_OFFSET]); + byte inCsVersion = bf.get(); + builder.setSalt(inSalt).setFlags(inFlags).setMerkleTreeOffset(inTreeOffset).setCsVersion(inCsVersion); + return builder.build(); + } + + /** + * Get FsVerity descriptor bytes + * + * @return bytes of descriptor + * @throws FsVerityDigestException if error + */ + public byte[] toByteArray() throws FsVerityDigestException { + ByteBuffer buffer = ByteBuffer.allocate(DESCRIPTOR_SIZE).order(ByteOrder.LITTLE_ENDIAN); + buffer.put(VERSION); + buffer.put(hashAlgorithm); + buffer.put(log2BlockSize); + if (this.saltSize > SALT_SIZE) { + throw new FsVerityDigestException("Salt is too long"); + } + buffer.put(this.saltSize); + buffer.putInt(signSize); + buffer.putLong(fileSize); + writeBytesWithSize(buffer, rawRootHash, ROOT_HASH_FILED_SIZE); + writeBytesWithSize(buffer, salt, SALT_SIZE); + buffer.putInt(flags); + writeBytesWithSize(buffer, null, RESERVED_SIZE_AFTER_FLAGS); + buffer.putLong(merkleTreeOffset); + writeBytesWithSize(buffer, null, RESERVED_SIZE_AFTER_TREE_OFFSET); + buffer.put(csVersion); + return buffer.array(); + } + + /** + * Get bytes for generate digest, first byte is CODE_SIGN_VERSION, sign size is 0, last 128 bytes is 0 + * + * @return bytes of descriptor + * @throws FsVerityDigestException if error + */ + public byte[] getByteForGenerateDigest() throws FsVerityDigestException { + ByteBuffer buffer = ByteBuffer.allocate(DESCRIPTOR_SIZE).order(ByteOrder.LITTLE_ENDIAN); + buffer.put(CODE_SIGN_VERSION); + buffer.put(hashAlgorithm); + buffer.put(log2BlockSize); + if (this.saltSize > SALT_SIZE) { + throw new FsVerityDigestException("Salt is too long"); + } + buffer.put(this.saltSize); + buffer.putInt(0); + buffer.putLong(fileSize); + writeBytesWithSize(buffer, rawRootHash, ROOT_HASH_FILED_SIZE); + writeBytesWithSize(buffer, salt, SALT_SIZE); + buffer.putInt(flags); + writeBytesWithSize(buffer, null, RESERVED_SIZE_AFTER_FLAGS); + buffer.putLong(merkleTreeOffset); + return buffer.array(); + } + + /** + * Write bytes to ByteBuffer with specific size + * + * @param buffer target buffer + * @param src bytes to write + * @param size size of written bytes, fill 0 if src bytes is long enough + */ + private void writeBytesWithSize(ByteBuffer buffer, byte[] src, int size) { + int pos = buffer.position(); + if (src != null) { + if (src.length > size) { + buffer.put(src, 0, size); + } else { + buffer.put(src); + } + } + buffer.position(pos + size); + } + + /** + * Builder of FsVerityDescriptor class + */ + public static class Builder { + private byte version = VERSION; + + private long fileSize; + + private byte hashAlgorithm; + + private byte log2BlockSize; + + private byte saltSize; + + private int signSize; + + private byte[] salt; + + private byte[] rawRootHash; + + private int flags; + + private long merkleTreeOffset; + + private byte csVersion; + + public Builder setVersion(byte version) { + this.version = version; + return this; + } + + public Builder setFileSize(long fileSize) { + this.fileSize = fileSize; + return this; + } + + public Builder setHashAlgorithm(byte hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + public Builder setLog2BlockSize(byte log2BlockSize) { + this.log2BlockSize = log2BlockSize; + return this; + } + + public Builder setSignSize(int signSize) { + this.signSize = signSize; + return this; + } + + public Builder setSaltSize(byte saltSize) { + this.saltSize = saltSize; + return this; + } + + public Builder setSalt(byte[] salt) { + this.salt = salt; + return this; + } + + public Builder setRawRootHash(byte[] rawRootHash) { + this.rawRootHash = rawRootHash; + return this; + } + + public Builder setFlags(int flags) { + this.flags = flags; + return this; + } + + public Builder setMerkleTreeOffset(long merkleTreeOffset) { + this.merkleTreeOffset = merkleTreeOffset; + return this; + } + + public Builder setCsVersion(byte csVersion) { + this.csVersion = csVersion; + return this; + } + + /** + * Create a FsVerityDescriptor object + * + * @return a FsVerityDescriptor object + */ + public FsVerityDescriptor build() { + return new FsVerityDescriptor(this); + } + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDescriptorWithSign.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDescriptorWithSign.java new file mode 100644 index 0000000000000000000000000000000000000000..6e3084b55d5b2c09f9ab451f0b540767def7dd15 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDescriptorWithSign.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * fsverity descriptor and signature + * 1) u32 type: 0x1 fsverity descriptor + * 2) u32 length: fsverity descriptor size + * 3) FsVerityDescriptor: fsVerityDescriptor + * 4) u8[] signature: signature after signing the data in byte array representation + * + * @since 2023/09/08 + */ +public class FsVerityDescriptorWithSign { + private int type = FsVerityDescriptor.FS_VERITY_DESCRIPTOR_TYPE; + + private int length; + + private FsVerityDescriptor fsVerityDescriptor; + + private byte[] signature = new byte[0]; + + /** + * Constructor of FsVerityDescriptorWithSign + * + * @param fsVerityDescriptor fs-verity descriptor + * @param signature signature + */ + public FsVerityDescriptorWithSign(FsVerityDescriptor fsVerityDescriptor, byte[] signature) { + this.fsVerityDescriptor = fsVerityDescriptor; + if (signature != null) { + this.signature = signature; + } + length = FsVerityDescriptor.DESCRIPTOR_SIZE + this.signature.length; + } + + /** + * Constructor of FsVerityDescriptorWithSign + * + * @param type fs-verity descriptor type + * @param length fs-verity descriptor with signature size + * @param fsVerityDescriptor fs-verity descriptor + * @param signature signature + */ + public FsVerityDescriptorWithSign(int type, int length, FsVerityDescriptor fsVerityDescriptor, byte[] signature) { + this.type = type; + this.length = length; + this.fsVerityDescriptor = fsVerityDescriptor; + this.signature = signature; + } + + /** + * Returns byte size of FsVerityDescriptorWithSign + * + * @return byte size of FsVerityDescriptorWithSign + */ + public int size() { + return Integer.BYTES * 2 + FsVerityDescriptor.DESCRIPTOR_SIZE + signature.length; + } + + /** + * Converts FsVerityDescriptorWithSign to a newly created byte array + * + * @return Byte array representation of FsVerityDescriptorWithSign + * @throws FsVerityDigestException if error + */ + public byte[] toByteArray() throws FsVerityDigestException { + ByteBuffer buffer = ByteBuffer.allocate(size()).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(type); + buffer.putInt(length); + buffer.put(fsVerityDescriptor.toByteArray()); + buffer.put(signature); + return buffer.array(); + } + + public FsVerityDescriptor getFsVerityDescriptor() { + return fsVerityDescriptor; + } + + public byte[] getSignature() { + return signature; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDigest.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDigest.java new file mode 100644 index 0000000000000000000000000000000000000000..cff3617c9f01f933191cca4c09fd2fd5c66cafbf --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityDigest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Format of FsVerity digest + * int8[8] magic "FSVerity" + * le16 digestAlgorithm sha256 = 1, sha512 = 2 + * le16 digestSize + * uint8[] digest + * + * @since 2023/06/05 + */ +public class FsVerityDigest { + private static final String FSVERITY_DIGEST_MAGIC = "FSVerity"; + + private static final int DIGEST_HEADER_SIZE = 12; + + /** + * Get formatted FsVerity digest + * + * @param algoID hash algorithm id + * @param digest raw digest computed from input + * @return formatted FsVerity digest bytes + */ + public static byte[] getFsVerityDigest(byte algoID, byte[] digest) { + int size = DIGEST_HEADER_SIZE + digest.length; + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.put(FSVERITY_DIGEST_MAGIC.getBytes(StandardCharsets.UTF_8)); + buffer.putShort(algoID); + buffer.putShort((short) digest.length); + buffer.put(digest); + return buffer.array(); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityGenerator.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..57501d09d6ab5e056e991a4fcd669340b362fa94 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityGenerator.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; +import com.ohos.hapsigntool.codesigning.utils.DigestUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchAlgorithmException; + +/** + * FsVerity data generator supper class + * + * @since 2023/06/05 + */ +public class FsVerityGenerator { + /** + * FsVerity hash algorithm + */ + private static final FsVerityHashAlgorithm FS_VERITY_HASH_ALGORITHM = FsVerityHashAlgorithm.SHA256; + + private static final byte LOG_2_OF_FSVERITY_HASH_PAGE_SIZE = 12; + + /** + * salt for hashing one page + */ + protected byte[] salt = null; + + private byte[] fsVerityDigest = null; + + private byte[] treeBytes = null; + + private byte[] rootHash = null; + + /** + * generate merkle tree of given input + * + * @param inputStream input stream for generate merkle tree + * @param size total size of input stream + * @param fsVerityHashAlgorithm hash algorithm for FsVerity + * @return merkle tree + * @throws FsVerityDigestException if error + */ + public MerkleTree generateMerkleTree(InputStream inputStream, long size, + FsVerityHashAlgorithm fsVerityHashAlgorithm) throws FsVerityDigestException { + MerkleTree merkleTree; + try (MerkleTreeBuilder builder = new MerkleTreeBuilder()) { + merkleTree = builder.generateMerkleTree(inputStream, size, fsVerityHashAlgorithm); + } catch (IOException e) { + throw new FsVerityDigestException("IOException: " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + throw new FsVerityDigestException("Invalid algorithm:" + e.getMessage()); + } + return merkleTree; + } + + /** + * generate FsVerity digest of given input + * + * @param inputStream input stream for generate FsVerity digest + * @param size total size of input stream + * @param fsvTreeOffset merkle tree raw bytes offset based on the start of file + * @throws FsVerityDigestException if error + */ + public void generateFsVerityDigest(InputStream inputStream, long size, long fsvTreeOffset) + throws FsVerityDigestException { + MerkleTree merkleTree; + if (size == 0) { + merkleTree = new MerkleTree(null, null, FS_VERITY_HASH_ALGORITHM); + } else { + merkleTree = generateMerkleTree(inputStream, size, FS_VERITY_HASH_ALGORITHM); + } + int flags = fsvTreeOffset == 0 ? 0 : FsVerityDescriptor.FLAG_STORE_MERKLE_TREE_OFFSET; + // sign size is 0, cs version is 0 + FsVerityDescriptor.Builder builder = new FsVerityDescriptor.Builder().setFileSize(size) + .setHashAlgorithm(FS_VERITY_HASH_ALGORITHM.getId()) + .setLog2BlockSize(LOG_2_OF_FSVERITY_HASH_PAGE_SIZE) + .setSaltSize((byte) getSaltSize()) + .setSalt(salt) + .setRawRootHash(merkleTree.rootHash) + .setFlags(flags) + .setMerkleTreeOffset(fsvTreeOffset); + byte[] fsVerityDescriptor = builder.build().getByteForGenerateDigest(); + byte[] digest; + try { + digest = DigestUtils.computeDigest(fsVerityDescriptor, FS_VERITY_HASH_ALGORITHM.getHashAlgorithm()); + } catch (NoSuchAlgorithmException e) { + throw new FsVerityDigestException("Invalid algorithm" + e.getMessage(), e); + } + fsVerityDigest = FsVerityDigest.getFsVerityDigest(FS_VERITY_HASH_ALGORITHM.getId(), digest); + treeBytes = merkleTree.tree; + rootHash = merkleTree.rootHash; + } + + /** + * Get FsVerity digest + * + * @return bytes of FsVerity digest + */ + public byte[] getFsVerityDigest() { + return fsVerityDigest; + } + + /** + * Get merkle tree in bytes + * + * @return bytes of merkle tree + */ + public byte[] getTreeBytes() { + return treeBytes; + } + + /** + * Get merkle tree rootHash in bytes + * + * @return bytes of merkle tree rootHash + */ + public byte[] getRootHash() { + return rootHash; + } + + public byte[] getSalt() { + return salt; + } + + /** + * Returns byte size of salt + * + * @return byte size of salt + */ + public int getSaltSize() { + return this.salt == null ? 0 : this.salt.length; + } + + /** + * Returns the id of fs-verity hash algorithm + * + * @return fs-verity hash algorithm id + */ + public static byte getFsVerityHashAlgorithm() { + return FS_VERITY_HASH_ALGORITHM.getId(); + } + + /** + * Returns the log2 of size of data and tree blocks + * + * @return log2 of size of data and tree blocks + */ + public static byte getLog2BlockSize() { + return LOG_2_OF_FSVERITY_HASH_PAGE_SIZE; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityHashAlgorithm.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityHashAlgorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..7d188413c321fe89efa35b064dfc8fd8597f7d47 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/FsVerityHashAlgorithm.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +/** + * FsVerity hash algorithm + * + * @since 2023/06/05 + */ +public enum FsVerityHashAlgorithm { + SHA256((byte) 1, "SHA-256", 256 / 8), + SHA512((byte) 2, "SHA-512", 512 / 8); + + private final byte id; + + private final String hashAlgorithm; + + private final int outputByteSize; + + FsVerityHashAlgorithm(byte id, String hashAlgorithm, int outputByteSize) { + this.id = id; + this.hashAlgorithm = hashAlgorithm; + this.outputByteSize = outputByteSize; + } + + public byte getId() { + return id; + } + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public int getOutputByteSize() { + return outputByteSize; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/MerkleTree.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/MerkleTree.java new file mode 100644 index 0000000000000000000000000000000000000000..eebfdd2813cb6a445240be1f3cf4315c9bfc1e00 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/MerkleTree.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +/** + * Merkle tree data + * + * @since 2023/06/05 + */ +public class MerkleTree { + /** + * root hash of merkle tree + */ + public final byte[] rootHash; + + /** + * content data of merkle tree + */ + public final byte[] tree; + + /** + * hash algorithm used for merkle tree + */ + public final FsVerityHashAlgorithm fsVerityHashAlgorithm; + + MerkleTree(byte[] rootHash, byte[] tree, FsVerityHashAlgorithm fsVerityHashAlgorithm) { + this.rootHash = rootHash; + this.tree = tree; + this.fsVerityHashAlgorithm = fsVerityHashAlgorithm; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/MerkleTreeBuilder.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/MerkleTreeBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..cbc198e9aa532b9a8cf62d9433040a47c16ff4ea --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/fsverity/MerkleTreeBuilder.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.fsverity; + +import com.ohos.hapsigntool.codesigning.utils.DigestUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Phaser; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Merkle tree builder + * + * @since 2023/06/05 + */ +public class MerkleTreeBuilder implements AutoCloseable { + private static final int FSVERITY_HASH_PAGE_SIZE = 4096; + + private static final long INPUTSTREAM_MAX_SIZE = 4503599627370496L; + + private static final int CHUNK_SIZE = 4096; + + private static final long MAX_READ_SIZE = 4194304L; + + private static final int MAX_PROCESSORS = 32; + + private static final int BLOCKINGQUEUE = 4; + + private static final int POOL_SIZE = Math.min(MAX_PROCESSORS, Runtime.getRuntime().availableProcessors()); + + private String mAlgorithm = "SHA-256"; + + private final ExecutorService mPools = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(BLOCKINGQUEUE), new ThreadPoolExecutor.CallerRunsPolicy()); + + /** + * Turn off multitasking + */ + public void close() { + this.mPools.shutdownNow(); + } + + /** + * set algorithm + * + * @param algorithm hash algorithm + */ + private void setAlgorithm(String algorithm) { + this.mAlgorithm = algorithm; + } + + /** + * translation inputStream to hash data + * + * @param inputStream input stream for generating merkle tree + * @param size total size of input stream + * @param outputBuffer hash data + * @throws IOException if error + */ + private void transInputStreamToHashData(InputStream inputStream, long size, ByteBuffer outputBuffer) + throws IOException { + if (size == 0) { + throw new IOException("Input size is empty"); + } else if (size > INPUTSTREAM_MAX_SIZE) { + throw new IOException("Input size is too long"); + } + int count = (int) getChunkCount(size, MAX_READ_SIZE); + int chunks = (int) getChunkCount(size, CHUNK_SIZE); + byte[][] hashes = new byte[chunks][]; + long readOffset = 0L; + Phaser tasks = new Phaser(1); + for (int i = 0; i < count; i++) { + long readLimit = Math.min(readOffset + MAX_READ_SIZE, size); + int readSize = (int) (readLimit - readOffset); + int fullChunkSize = (int) getFullChunkSize(readSize, CHUNK_SIZE, CHUNK_SIZE); + + ByteBuffer byteBuffer = ByteBuffer.allocate(fullChunkSize); + byte[] buffer = new byte[CHUNK_SIZE]; + int num; + int offset = 0; + int len = CHUNK_SIZE; + while ((num = inputStream.read(buffer, 0, len)) > 0) { + byteBuffer.put(buffer, 0, num); + offset += num; + len = Math.min(CHUNK_SIZE, readSize - offset); + if (len <= 0 || offset == readSize) { + break; + } + } + if (offset != readSize) { + throw new IOException("IOException read buffer from input errorLHJ."); + } + byteBuffer.flip(); + int readChunkIndex = (int) getFullChunkSize(MAX_READ_SIZE, CHUNK_SIZE, i); + runHashTask(hashes, tasks, byteBuffer, readChunkIndex); + readOffset += readSize; + } + tasks.arriveAndAwaitAdvance(); + for (byte[] hash : hashes) { + outputBuffer.put(hash, 0, hash.length); + } + } + + /** + * split buffer by begin and end information + * + * @param buffer original buffer + * @param begin begin position + * @param end end position + * @return slice buffer + */ + private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { + ByteBuffer tempBuffer = buffer.duplicate(); + tempBuffer.position(0); + tempBuffer.limit(end); + tempBuffer.position(begin); + return tempBuffer.slice(); + } + + /** + * calculate merkle tree level and size by data size and digest size + * + * @param dataSize original data size + * @param digestSize algorithm data size + * @return level offset list, contains the offset of + * each level from the root node to the leaf node + */ + private static int[] getOffsetArrays(long dataSize, int digestSize) { + ArrayList levelSize = getLevelSize(dataSize, digestSize); + int[] levelOffset = new int[levelSize.size() + 1]; + levelOffset[0] = 0; + for (int i = 0; i < levelSize.size(); i++) { + levelOffset[i + 1] = levelOffset[i] + Math.toIntExact(levelSize.get(levelSize.size() - i - 1)); + } + return levelOffset; + } + + /** + * calculate data size list by data size and digest size + * + * @param dataSize original data size + * @param digestSize algorithm data size + * @return data size list, contains the offset of + * each level from the root node to the leaf node + */ + private static ArrayList getLevelSize(long dataSize, int digestSize) { + ArrayList levelSize = new ArrayList<>(); + long fullChunkSize = 0L; + long originalDataSize = dataSize; + do { + fullChunkSize = getFullChunkSize(originalDataSize, CHUNK_SIZE, digestSize); + long size = getFullChunkSize(fullChunkSize, CHUNK_SIZE, CHUNK_SIZE); + levelSize.add(size); + originalDataSize = fullChunkSize; + } while (fullChunkSize > CHUNK_SIZE); + return levelSize; + } + + private void runHashTask(byte[][] hashes, Phaser tasks, ByteBuffer buffer, int readChunkIndex) { + Runnable task = () -> { + int offset = 0; + int bufferSize = buffer.capacity(); + int index = readChunkIndex; + while (offset < bufferSize) { + ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE); + byte[] tempByte = new byte[CHUNK_SIZE]; + chunk.get(tempByte); + try { + hashes[index++] = DigestUtils.computeDigest(tempByte, this.mAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + offset += CHUNK_SIZE; + } + tasks.arriveAndDeregister(); + }; + tasks.register(); + this.mPools.execute(task); + } + + /** + * hash data of buffer + * + * @param inputBuffer original data + * @param outputBuffer hash data + */ + private void transInputDataToHashData(ByteBuffer inputBuffer, ByteBuffer outputBuffer) { + long size = inputBuffer.capacity(); + int chunks = (int) getChunkCount(size, CHUNK_SIZE); + byte[][] hashes = new byte[chunks][]; + Phaser tasks = new Phaser(1); + long readOffset = 0L; + int startChunkIndex = 0; + while (readOffset < size) { + long readLimit = Math.min(readOffset + MAX_READ_SIZE, size); + ByteBuffer buffer = slice(inputBuffer, (int) readOffset, (int) readLimit); + buffer.rewind(); + int readChunkIndex = startChunkIndex; + runHashTask(hashes, tasks, buffer, readChunkIndex); + int readSize = (int) (readLimit - readOffset); + startChunkIndex += (int) getChunkCount(readSize, CHUNK_SIZE); + readOffset += readSize; + } + tasks.arriveAndAwaitAdvance(); + for (byte[] hash : hashes) { + outputBuffer.put(hash, 0, hash.length); + } + } + + /** + * generate merkle tree of given input + * + * @param inputStream input stream for generate merkle tree + * @param size total size of input stream + * @param fsVerityHashAlgorithm hash algorithm for FsVerity + * @return merkle tree + * @throws IOException if error + * @throws NoSuchAlgorithmException if error + */ + public MerkleTree generateMerkleTree(InputStream inputStream, long size, + FsVerityHashAlgorithm fsVerityHashAlgorithm) throws IOException, NoSuchAlgorithmException { + setAlgorithm(fsVerityHashAlgorithm.getHashAlgorithm()); + int digestSize = fsVerityHashAlgorithm.getOutputByteSize(); + int[] offsetArrays = getOffsetArrays(size, digestSize); + ByteBuffer allHashBuffer = ByteBuffer.allocate(offsetArrays[offsetArrays.length - 1]); + generateHashDataByInputData(inputStream, size, allHashBuffer, offsetArrays, digestSize); + generateHashDataByHashData(allHashBuffer, offsetArrays, digestSize); + return getMerkleTree(allHashBuffer, size, fsVerityHashAlgorithm); + } + + /** + * translation inputBuffer arrays to hash ByteBuffer + * + * @param inputStream input stream for generate merkle tree + * @param size total size of input stream + * @param outputBuffer hash data + * @param offsetArrays level offset + * @param digestSize algorithm output byte size + * @throws IOException if error + */ + private void generateHashDataByInputData(InputStream inputStream, long size, ByteBuffer outputBuffer, + int[] offsetArrays, int digestSize) throws IOException { + int inputDataOffsetBegin = offsetArrays[offsetArrays.length - 2]; + int inputDataOffsetEnd = offsetArrays[offsetArrays.length - 1]; + ByteBuffer hashBuffer = slice(outputBuffer, inputDataOffsetBegin, inputDataOffsetEnd); + transInputStreamToHashData(inputStream, size, hashBuffer); + dataRoundupChunkSize(hashBuffer, size, digestSize); + } + + /** + * get buffer data by level offset, transforms digest data, save in another + * memory + * + * @param buffer hash data + * @param offsetArrays level offset + * @param digestSize algorithm output byte size + */ + private void generateHashDataByHashData(ByteBuffer buffer, int[] offsetArrays, int digestSize) { + for (int i = offsetArrays.length - 3; i >= 0; i--) { + ByteBuffer generateHashBuffer = slice(buffer, offsetArrays[i], offsetArrays[i + 1]); + ByteBuffer originalHashBuffer = slice(buffer.asReadOnlyBuffer(), offsetArrays[i + 1], offsetArrays[i + 2]); + transInputDataToHashData(originalHashBuffer, generateHashBuffer); + dataRoundupChunkSize(generateHashBuffer, originalHashBuffer.capacity(), digestSize); + } + } + + /** + * generate merkle tree of given input + * + * @param dataBuffer tree data memory block + * @param inputDataSize total size of input stream + * @param fsVerityHashAlgorithm hash algorithm for FsVerity + * @return merkle tree + * @throws NoSuchAlgorithmException if error + */ + private MerkleTree getMerkleTree(ByteBuffer dataBuffer, long inputDataSize, + FsVerityHashAlgorithm fsVerityHashAlgorithm) throws NoSuchAlgorithmException { + int digestSize = fsVerityHashAlgorithm.getOutputByteSize(); + dataBuffer.flip(); + byte[] rootHash = null; + byte[] tree = null; + if (inputDataSize < FSVERITY_HASH_PAGE_SIZE) { + ByteBuffer fsVerityHashPageBuffer = slice(dataBuffer, 0, digestSize); + rootHash = new byte[digestSize]; + fsVerityHashPageBuffer.get(rootHash); + } else { + tree = dataBuffer.array(); + ByteBuffer fsVerityHashPageBuffer = slice(dataBuffer.asReadOnlyBuffer(), 0, FSVERITY_HASH_PAGE_SIZE); + byte[] fsVerityHashPage = new byte[FSVERITY_HASH_PAGE_SIZE]; + fsVerityHashPageBuffer.get(fsVerityHashPage); + rootHash = DigestUtils.computeDigest(fsVerityHashPage, this.mAlgorithm); + } + return new MerkleTree(rootHash, tree, fsVerityHashAlgorithm); + } + + /** + * generate merkle tree of given input + * + * @param data original data + * @param originalDataSize data size + * @param digestSize algorithm output byte size + */ + private void dataRoundupChunkSize(ByteBuffer data, long originalDataSize, int digestSize) { + long fullChunkSize = getFullChunkSize(originalDataSize, CHUNK_SIZE, digestSize); + int diffValue = (int) (fullChunkSize % CHUNK_SIZE); + if (diffValue > 0) { + byte[] padding = new byte[CHUNK_SIZE - diffValue]; + data.put(padding, 0, padding.length); + } + } + + /** + * get mount of chunks to store data + * + * @param dataSize data size + * @param divisor split chunk size + * @return chunk count + */ + private static long getChunkCount(long dataSize, long divisor) { + return (long) Math.ceil((double) dataSize / (double) divisor); + } + + /** + * get total size of chunk to store data + * + * @param dataSize data size + * @param divisor split chunk size + * @param multiplier chunk multiplier + * @return chunk size + */ + private static long getFullChunkSize(long dataSize, long divisor, long multiplier) { + return getChunkCount(dataSize, divisor) * multiplier; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/BcSignedDataGenerator.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/BcSignedDataGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..2d7591a8bf603dde97e0c30472d52c907a24ad65 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/BcSignedDataGenerator.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.sign; + +import com.ohos.hapsigntool.codesigning.exception.CodeSignException; +import com.ohos.hapsigntool.codesigning.utils.CmsUtils; +import com.ohos.hapsigntool.codesigning.utils.DigestUtils; +import com.ohos.hapsigntool.hap.config.SignerConfig; +import com.ohos.hapsigntool.hap.entity.Pair; +import com.ohos.hapsigntool.hap.sign.ContentDigestAlgorithm; +import com.ohos.hapsigntool.hap.sign.SignatureAlgorithm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.BERSet; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSet; +import org.bouncycastle.asn1.cms.Time; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.ContentInfo; +import org.bouncycastle.asn1.pkcs.IssuerAndSerialNumber; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.SignedData; +import org.bouncycastle.asn1.pkcs.SignerInfo; +import org.bouncycastle.cert.jcajce.JcaX509CRLHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.SignatureAlgorithmIdentifierFinder; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CRLException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Date; +import java.util.List; + +/** + * BC implementation + * + * @since 2023/06/05 + */ +public class BcSignedDataGenerator implements SignedDataGenerator { + private static final Logger LOGGER = LogManager.getLogger(BcSignedDataGenerator.class); + + private static final SignatureAlgorithmIdentifierFinder SIGN_ALG_ID_FINDER + = new DefaultSignatureAlgorithmIdentifierFinder(); + + private static final DigestAlgorithmIdentifierFinder DIGEST_ALG_ID_FINDER + = new DefaultDigestAlgorithmIdentifierFinder(); + + @Override + public byte[] generateSignedData(byte[] content, SignerConfig signConfig) throws CodeSignException { + if (content == null) { + throw new CodeSignException("Verity digest is null"); + } + Pair pairDigestAndSignInfo = getSignInfo(content, signConfig); + SignedData signedData = new SignedData(new ASN1Integer(1), pairDigestAndSignInfo.getFirst(), + new ContentInfo(PKCSObjectIdentifiers.data, null), createBerSetFromLst(signConfig.getCertificates()), + createBerSetFromLst(signConfig.getX509CRLs()), pairDigestAndSignInfo.getSecond()); + return encodingUnsignedData(content, signedData); + } + + private Pair getSignInfo(byte[] content, SignerConfig signConfig) throws CodeSignException { + ASN1EncodableVector signInfoVector = new ASN1EncodableVector(); + ASN1EncodableVector digestVector = new ASN1EncodableVector(); + for (SignatureAlgorithm signAlgorithm : signConfig.getSignatureAlgorithms()) { + SignerInfo signInfo = createSignInfo(signAlgorithm, content, signConfig); + signInfoVector.add(signInfo); + digestVector.add(signInfo.getDigestAlgorithm()); + LOGGER.info("Create a sign info successfully."); + } + return Pair.create(new DERSet(digestVector), new DERSet(signInfoVector)); + } + + private SignerInfo createSignInfo(SignatureAlgorithm signAlgorithm, byte[] unsignedDataDigest, + SignerConfig signConfig) throws CodeSignException { + ContentDigestAlgorithm hashAlgorithm = signAlgorithm.getContentDigestAlgorithm(); + byte[] digest = computeDigest(unsignedDataDigest, hashAlgorithm.name()); + ASN1Set authed = getPKCS9Attributes(digest); + byte[] codeAuthed = getEncoded(authed); + Pair signPair = signAlgorithm.getSignatureAlgAndParams(); + byte[] signBytes = signConfig.getSigner().getSignature(codeAuthed, signPair.getFirst(), signPair.getSecond()); + if (signBytes == null) { + throw new CodeSignException("get signature failed"); + } + if (signConfig.getCertificates().isEmpty()) { + throw new CodeSignException("No certificates configured for sign"); + } + X509Certificate cert = signConfig.getCertificates().get(0); + if (!verifySignFromServer(cert.getPublicKey(), signBytes, signPair, codeAuthed)) { + throw new CodeSignException("verifySignatureFromServer failed"); + } + JcaX509CertificateHolder certificateHolder = getJcaX509CertificateHolder(cert); + return new SignerInfo(new ASN1Integer(1), + new IssuerAndSerialNumber(certificateHolder.getIssuer(), certificateHolder.getSerialNumber()), + DIGEST_ALG_ID_FINDER.find(hashAlgorithm.getDigestAlgorithm()), authed, + SIGN_ALG_ID_FINDER.find(signPair.getFirst()), new DEROctetString(signBytes), null); + } + + private byte[] computeDigest(byte[] unsignedDataDigest, String algorithm) throws CodeSignException { + byte[] digest; + try { + digest = DigestUtils.computeDigest(unsignedDataDigest, algorithm); + } catch (NoSuchAlgorithmException e) { + throw new CodeSignException("Invalid algorithm" + e.getMessage(), e); + } + return digest; + } + + private byte[] getEncoded(ASN1Set authed) throws CodeSignException { + byte[] codeAuthed; + try { + codeAuthed = authed.getEncoded(); + } catch (IOException e) { + throw new CodeSignException("cannot encode authed", e); + } + return codeAuthed; + } + + private JcaX509CRLHolder getJcaX509CRLHolder(X509CRL crl) throws CodeSignException { + JcaX509CRLHolder crlHolder; + try { + crlHolder = new JcaX509CRLHolder(crl); + } catch (CRLException e) { + throw new CodeSignException("Create crl failed", e); + } + return crlHolder; + } + + private JcaX509CertificateHolder getJcaX509CertificateHolder(X509Certificate cert) throws CodeSignException { + JcaX509CertificateHolder certificateHolder; + try { + certificateHolder = new JcaX509CertificateHolder(cert); + } catch (CertificateEncodingException e) { + throw new CodeSignException("Create sign info failed", e); + } + return certificateHolder; + } + + private ASN1Set getPKCS9Attributes(byte[] digest) { + ASN1EncodableVector table = new ASN1EncodableVector(); + Attribute signingTimeAttr = new Attribute(PKCSObjectIdentifiers.pkcs_9_at_signingTime, + new DERSet(new Time(new Date()))); + Attribute contentTypeAttr = new Attribute(PKCSObjectIdentifiers.pkcs_9_at_contentType, + new DERSet(PKCSObjectIdentifiers.data)); + Attribute messageDigestAttr = new Attribute(PKCSObjectIdentifiers.pkcs_9_at_messageDigest, + new DERSet(new DEROctetString(digest))); + table.add(signingTimeAttr); + table.add(contentTypeAttr); + table.add(messageDigestAttr); + return new DERSet(table); + } + + private boolean verifySignFromServer(PublicKey publicKey, byte[] signBytes, + Pair signPair, byte[] authed) throws CodeSignException { + try { + Signature signature = Signature.getInstance(signPair.getFirst()); + signature.initVerify(publicKey); + if (signPair.getSecond() != null) { + signature.setParameter(signPair.getSecond()); + } + signature.update(authed); + if (!signature.verify(signBytes)) { + throw new CodeSignException("Signature verify failed"); + } + return true; + } catch (InvalidKeyException | SignatureException e) { + LOGGER.error("The generated signature could not be verified " + " using the public key in the certificate", + e); + } catch (NoSuchAlgorithmException e) { + LOGGER.error("The generated signature " + signPair.getFirst() + + " could not be verified using the public key in the certificate", e); + } catch (InvalidAlgorithmParameterException e) { + LOGGER.error("The generated signature " + signPair.getSecond() + + " could not be verified using the public key in the certificate", e); + } + return false; + } + + private ASN1Set createBerSetFromLst(List lists) throws CodeSignException { + if (lists == null || lists.size() == 0) { + return null; + } + ASN1EncodableVector vector = new ASN1EncodableVector(); + for (Object obj : lists) { + if (obj instanceof X509CRL) { + vector.add(getJcaX509CRLHolder((X509CRL) obj).toASN1Structure()); + } else if (obj instanceof X509Certificate) { + vector.add(getJcaX509CertificateHolder((X509Certificate) obj).toASN1Structure()); + } + } + return new BERSet(vector); + } + + private byte[] encodingUnsignedData(byte[] unsignedDataDigest, SignedData signedData) throws CodeSignException { + byte[] signResult; + try { + ContentInfo contentInfo = new ContentInfo(PKCSObjectIdentifiers.signedData, signedData); + signResult = contentInfo.getEncoded(ASN1Encoding.DER); + } catch (IOException e) { + throw new CodeSignException("failed to encode unsigned data!", e); + } + verifySignResult(unsignedDataDigest, signResult); + return signResult; + } + + private void verifySignResult(byte[] unsignedDataDigest, byte[] signResult) throws CodeSignException { + boolean result = false; + try { + result = CmsUtils.verifySignDataWithUnsignedDataDigest(unsignedDataDigest, signResult); + } catch (CMSException e) { + throw new CodeSignException("failed to verify signed data and unsigned data digest", e); + } + if (!result) { + throw new CodeSignException("PKCS cms data did not verify"); + } + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java new file mode 100644 index 0000000000000000000000000000000000000000..e9c8b1524818df186eeea30929bb57083cb40a83 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CentralDirectory.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.sign; + +import org.bouncycastle.util.Strings; + +import java.util.Locale; +import java.util.zip.ZipEntry; + +/** + * Central directory structure + * further reference to Zip Format + * + * @since 2023/09/14 + */ +public class CentralDirectory { + /** + * Byte size of all fields before "compression method" in central directory structure + */ + public static final int BYTE_SIZE_BEFORE_COMPRESSION_METHOD = 10; + + /** + * Byte size of all fields between "compression method" and "file comment length" in central directory structure + */ + public static final int BYTE_SIZE_BETWEEN_COMPRESSION_MODE_AND_FILE_SIZE = 16; + + /** + * Byte size of all fields between "file comment length" and + * "relative offset of local header" in central directory structure + */ + public static final int BYTE_SIZE_BETWEEN_FILE_COMMENT_LENGTH_AND_LOCHDR_RELATIVE_OFFSET = 8; + + private final char compressionMethod; + + private final char fileNameLength; + + private final char extraFieldLength; + + private final char fileCommentLength; + + private final int relativeOffsetOfLocalHeader; + + private final byte[] fileName; + + public CentralDirectory(Builder builder) { + this.compressionMethod = builder.compressionMethod; + this.fileNameLength = builder.fileNameLength; + this.extraFieldLength = builder.extraFieldLength; + this.fileCommentLength = builder.fileCommentLength; + this.relativeOffsetOfLocalHeader = builder.relativeOffsetOfLocalHeader; + this.fileName = builder.fileName; + } + + /** + * Return true if entry is an executable file, i.e. abc or so + * + * @return true if entry is an executable file + */ + public boolean isCodeFile() { + return this.getFileName().endsWith(".abc") || this.getFileName().endsWith(".so"); + } + + /** + * Return true if zip entry is uncompressed + * + * @return true if zip entry is uncompressed + */ + public boolean isUncompressed() { + return this.compressionMethod == ZipEntry.STORED; + } + + public String getFileName() { + return Strings.fromByteArray(this.fileName); + } + + public int getRelativeOffsetOfLocalHeader() { + return relativeOffsetOfLocalHeader; + } + + /** + * Sum byte size of three variable fields: file name, extra field, file comment + * + * @return Sum byte size of three variable fields + */ + public char getFileNameLength() { + return fileNameLength; + } + + public char getExtraFieldLength() { + return extraFieldLength; + } + + /** + * Return a string representation of the object + * + * @return string representation of the object + */ + public String toString() { + return String.format(Locale.ROOT, + "CentralDirectory:compressionMode(%d), fileName(%s), relativeOffsetOfLocalHeader(%d), " + + "fileNameLength(%d), extraFieldLength(%d), fileCommentLength(%d)", (int) this.compressionMethod, + this.getFileName(), this.relativeOffsetOfLocalHeader, (int) this.fileNameLength, + (int) this.extraFieldLength, (int) this.fileCommentLength); + } + + /** + * Builder of CentralDirectory class + */ + public static class Builder { + private char compressionMethod; + + private char fileNameLength; + + private char extraFieldLength; + + private char fileCommentLength; + + private int relativeOffsetOfLocalHeader; + + private byte[] fileName; + + public Builder setCompressionMethod(char compressionMethod) { + this.compressionMethod = compressionMethod; + return this; + } + + public Builder setFileNameLength(char fileNameLength) { + this.fileNameLength = fileNameLength; + return this; + } + + public Builder setExtraFieldLength(char extraFieldLength) { + this.extraFieldLength = extraFieldLength; + return this; + } + + public Builder setFileCommentLength(char fileCommentLength) { + this.fileCommentLength = fileCommentLength; + return this; + } + + public Builder setRelativeOffsetOfLocalHeader(int relativeOffsetOfLocalHeader) { + this.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader; + return this; + } + + public Builder setFileName(byte[] fileName) { + this.fileName = fileName; + return this; + } + + /** + * Create a CentralDirectory object + * + * @return a CentralDirectory object + */ + public CentralDirectory build() { + return new CentralDirectory(this); + } + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java new file mode 100644 index 0000000000000000000000000000000000000000..527655c192a52b097048bc47a55064afcf55cb63 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/CodeSigning.java @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.sign; + +import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlock; +import com.ohos.hapsigntool.codesigning.datastructure.ElfSignBlock; +import com.ohos.hapsigntool.codesigning.datastructure.Extension; +import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment; +import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension; +import com.ohos.hapsigntool.codesigning.datastructure.SignInfo; +import com.ohos.hapsigntool.codesigning.exception.CodeSignException; +import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptor; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptorWithSign; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator; +import com.ohos.hapsigntool.codesigning.utils.HapUtils; +import com.ohos.hapsigntool.hap.config.SignerConfig; +import com.ohos.hapsigntool.hap.entity.Pair; +import com.ohos.hapsigntool.hap.exception.HapFormatException; +import com.ohos.hapsigntool.signer.LocalSigner; +import com.ohos.hapsigntool.zip.RandomAccessFileZipDataInput; +import com.ohos.hapsigntool.zip.ZipDataInput; +import com.ohos.hapsigntool.zip.ZipFileInfo; +import com.ohos.hapsigntool.zip.ZipUtils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * core functions of code signing + * + * @since 2023/06/05 + */ +public class CodeSigning { + /** + * Only hap and hsp bundle supports code signing, concatenate then with '/' for verification usage + */ + public static final String SUPPORT_FILE_FORM = "hap/hsp"; + + /** + * Only elf file supports bin code signing, concatenate then with '/' for verification usage + */ + public static final String SUPPORT_BIN_FILE_FORM = "elf"; + + /** + * Defined entry name of hap file + */ + public static final String HAP_SIGNATURE_ENTRY_NAME = "Hap"; + + private static final Logger LOGGER = LogManager.getLogger(CodeSigning.class); + + private static final String NATIVE_LIB_AN_SUFFIX = ".an"; + + private static final String NATIVE_LIB_SO_SUFFIX = ".so"; + + private final List extractedNativeLibSuffixs = new ArrayList<>(); + + private final SignerConfig signConfig; + + private CodeSignBlock codeSignBlock; + + private long timestamp = 0L; + + /** + * provide code sign functions to sign a hap + * + * @param signConfig configuration of sign + */ + public CodeSigning(SignerConfig signConfig) { + this.signConfig = signConfig; + } + + /** + * Sign the given elf file, and pack all signature into output file + * + * @param input file to sign + * @param offset position of codesign block based on start of the file + * @param inForm file's format + * @return byte array of code sign block + * @throws CodeSignException code signing exception + * @throws IOException io error + * @throws FsVerityDigestException computing FsVerity digest error + */ + public byte[] getElfCodeSignBlock(File input, long offset, String inForm) + throws CodeSignException, FsVerityDigestException, IOException { + if (!SUPPORT_BIN_FILE_FORM.contains(inForm)) { + throw new CodeSignException("file's format is unsupported"); + } + long fileSize = input.length(); + int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset); + long fsvTreeOffset = offset + Integer.BYTES * 2 + paddingSize; + try (FileInputStream inputStream = new FileInputStream(input)) { + FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); + fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset); + byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest(); + byte[] signature = generateSignature(fsVerityDigest); + // add fs-verify info + FsVerityDescriptor.Builder fsdbuilder = new FsVerityDescriptor.Builder().setFileSize(fileSize) + .setHashAlgorithm(FsVerityGenerator.getFsVerityHashAlgorithm()) + .setLog2BlockSize(FsVerityGenerator.getLog2BlockSize()) + .setSaltSize((byte) fsVerityGenerator.getSaltSize()) + .setSignSize(signature.length) + .setFileSize(fileSize) + .setSalt(fsVerityGenerator.getSalt()) + .setRawRootHash(fsVerityGenerator.getRootHash()) + .setFlags(FsVerityDescriptor.FLAG_STORE_MERKLE_TREE_OFFSET) + .setMerkleTreeOffset(fsvTreeOffset) + .setCsVersion(FsVerityDescriptor.CODE_SIGN_VERSION); + FsVerityDescriptorWithSign fsVerityDescriptorWithSign = new FsVerityDescriptorWithSign(fsdbuilder.build(), + signature); + byte[] treeBytes = fsVerityGenerator.getTreeBytes(); + ElfSignBlock signBlock = new ElfSignBlock(paddingSize, treeBytes, fsVerityDescriptorWithSign); + LOGGER.info("Sign elf successfully."); + return signBlock.toByteArray(); + } + } + + /** + * Sign the given hap file, and pack all signature into output file + * + * @param input file to sign + * @param offset position of codesign block based on start of the file + * @param inForm file's format + * @return byte array of code sign block + * @throws CodeSignException code signing exception + * @throws IOException io error + * @throws HapFormatException hap format invalid + * @throws FsVerityDigestException computing FsVerity digest error + */ + public byte[] getCodeSignBlock(File input, long offset, String inForm) + throws CodeSignException, IOException, HapFormatException, FsVerityDigestException { + LOGGER.info("Start to sign code."); + if (SUPPORT_BIN_FILE_FORM.contains(inForm)) { + return getElfCodeSignBlock(input, offset, inForm); + } + if (!SUPPORT_FILE_FORM.contains(inForm)) { + throw new CodeSignException("file's format is unsupported"); + } + long dataSize = computeDataSize(input); + timestamp = System.currentTimeMillis(); + // generate CodeSignBlock + this.codeSignBlock = new CodeSignBlock(); + // compute merkle tree offset, replace with computeMerkleTreeOffset if fs-verity descriptor supports + long fsvTreeOffset = this.codeSignBlock.computeMerkleTreeOffset(offset); + // update fs-verity segment + FsVerityInfoSegment fsVerityInfoSegment = new FsVerityInfoSegment(FsVerityDescriptor.VERSION, + FsVerityGenerator.getFsVerityHashAlgorithm(), FsVerityGenerator.getLog2BlockSize()); + this.codeSignBlock.setFsVerityInfoSegment(fsVerityInfoSegment); + + LOGGER.debug("Sign hap."); + try (FileInputStream inputStream = new FileInputStream(input)) { + Pair hapSignInfoAndMerkleTreeBytesPair = signFile(inputStream, dataSize, true, + fsvTreeOffset); + // update hap segment in CodeSignBlock + this.codeSignBlock.getHapInfoSegment().setSignInfo(hapSignInfoAndMerkleTreeBytesPair.getFirst()); + // Insert merkle tree bytes into code sign block + this.codeSignBlock.addOneMerkleTree(HAP_SIGNATURE_ENTRY_NAME, + hapSignInfoAndMerkleTreeBytesPair.getSecond()); + } + // update native lib info segment in CodeSignBlock + signNativeLibs(input); + + // last update codeSignBlock before generating its byte array representation + updateCodeSignBlock(this.codeSignBlock); + + // complete code sign block byte array here + byte[] generated = this.codeSignBlock.generateCodeSignBlockByte(fsvTreeOffset); + LOGGER.info("Sign successfully."); + return generated; + } + + private long computeDataSize(File file) throws IOException, HapFormatException { + long centralDirectoryOffset; + int centralDirectorySize; + int centralDirectoryEntryCount; + // parse central directory + try (RandomAccessFile outputHap = new RandomAccessFile(file, "rw")) { + ZipDataInput outputHapIn = new RandomAccessFileZipDataInput(outputHap); + ZipFileInfo zipInfo = ZipUtils.findZipInfo(outputHapIn); + centralDirectoryOffset = zipInfo.getCentralDirectoryOffset(); + centralDirectorySize = zipInfo.getCentralDirectorySize(); + centralDirectoryEntryCount = zipInfo.getCentralDirectoryEntryCount(); + } + // centralDirectoryOffset is where all data ends, including abc/so/an and resources + try (FileInputStream input = new FileInputStream(file)) { + input.skip(centralDirectoryOffset); + byte[] centralDirectoryBuffer = new byte[centralDirectorySize]; + input.read(centralDirectoryBuffer); + List cdList = parseCentralDirectory(centralDirectoryBuffer, centralDirectoryEntryCount); + long dataSize = 0L; + for (CentralDirectory entry : cdList) { + if (entry.isCodeFile() && entry.isUncompressed()) { + continue; + } + // if the first file is not uncompressed abc or so, set dataSize to zero + if (entry.getRelativeOffsetOfLocalHeader() == 0) { + dataSize = 0; + break; + } + // the first entry which is not abc/so/an is found, return its data offset + dataSize = entry.getRelativeOffsetOfLocalHeader() + JarFile.LOCHDR + entry.getFileNameLength() + + entry.getExtraFieldLength(); + break; + } + if ((dataSize % CodeSignBlock.PAGE_SIZE_4K) != 0) { + throw new HapFormatException( + String.format(Locale.ROOT, "Invalid dataSize(%d), not a multiple of 4096", dataSize)); + } + return dataSize; + } + } + + private void signNativeLibs(File input) throws IOException, FsVerityDigestException, CodeSignException { + // 'an' libs are always signed + extractedNativeLibSuffixs.add(NATIVE_LIB_AN_SUFFIX); + if (HapUtils.checkCompressNativeLibs(input)) { + LOGGER.info("compressNativeLibs equals true, sign so libs as well."); + // sign so libs only if compressNativeLibs equals true + extractedNativeLibSuffixs.add(NATIVE_LIB_SO_SUFFIX); + } + + // sign native files + try (JarFile inputJar = new JarFile(input, false)) { + List entryNames = getNativeEntriesFromHap(inputJar); + if (entryNames.isEmpty()) { + LOGGER.info("No native libs."); + return; + } + List> nativeLibInfoList = signFilesFromJar(entryNames, inputJar); + // update SoInfoSegment in CodeSignBlock + this.codeSignBlock.getSoInfoSegment().setSoInfoList(nativeLibInfoList); + } + } + + /** + * Get entry name of all native files in hap + * + * @param hap the given hap + * @return list of entry name + */ + private List getNativeEntriesFromHap(JarFile hap) { + List result = new ArrayList<>(); + for (Enumeration e = hap.entries(); e.hasMoreElements();) { + JarEntry entry = e.nextElement(); + if (!entry.isDirectory()) { + if (!isNativeFile(entry.getName())) { + continue; + } + result.add(entry.getName()); + } + } + return result; + } + + /** + * Check whether the entry is a native file + * + * @param entryName the name of entry + * @return true if it is a native file, and false otherwise + */ + private boolean isNativeFile(String entryName) { + for (String suffix : extractedNativeLibSuffixs) { + if (entryName.endsWith(suffix)) { + return true; + } + } + return false; + } + + /** + * Sign specific entries in a hap + * + * @param entryNames list of entries which need to be signed + * @param hap input hap + * @return sign info and merkle tree of each file + * @throws IOException io error + * @throws FsVerityDigestException computing FsVerity digest error + * @throws CodeSignException sign error + */ + private List> signFilesFromJar(List entryNames, JarFile hap) + throws IOException, FsVerityDigestException, CodeSignException { + List> nativeLibInfoList = new ArrayList<>(); + for (String name : entryNames) { + LOGGER.debug("Sign entry name = " + name); + JarEntry inEntry = hap.getJarEntry(name); + try (InputStream inputStream = hap.getInputStream(inEntry)) { + long fileSize = inEntry.getSize(); + // We don't store merkle tree in code signing of native libs + // Therefore, the second value of pair returned is ignored + Pair pairSignInfoAndMerkleTreeBytes = signFile(inputStream, fileSize, false, 0); + nativeLibInfoList.add(Pair.create(name, pairSignInfoAndMerkleTreeBytes.getFirst())); + } + } + return nativeLibInfoList; + } + + /** + * Sign a file from input stream + * + * @param inputStream input stream of a file + * @param fileSize size of the file + * @param storeTree whether to store merkle tree in signed info + * @param fsvTreeOffset merkle tree raw bytes offset based on the start of file + * @return pair of signature and tree + * @throws FsVerityDigestException computing FsVerity Digest error + * @throws CodeSignException signing error + */ + public Pair signFile(InputStream inputStream, long fileSize, boolean storeTree, + long fsvTreeOffset) throws FsVerityDigestException, CodeSignException { + FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); + fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset); + byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest(); + byte[] signature = generateSignature(fsVerityDigest); + int flags = 0; + if (storeTree) { + flags = SignInfo.FLAG_MERKLE_TREE_INCLUDED; + } + SignInfo signInfo = new SignInfo(fsVerityGenerator.getSaltSize(), flags, fileSize, fsVerityGenerator.getSalt(), + signature); + // if store merkle tree in sign info + if (storeTree) { + int merkleTreeSize = fsVerityGenerator.getTreeBytes() == null ? 0 : fsVerityGenerator.getTreeBytes().length; + Extension merkleTreeExtension = new MerkleTreeExtension(merkleTreeSize, fsvTreeOffset, + fsVerityGenerator.getRootHash()); + signInfo.addExtension(merkleTreeExtension); + } + return Pair.create(signInfo, fsVerityGenerator.getTreeBytes()); + } + + private byte[] generateSignature(byte[] signedData) throws CodeSignException { + // signConfig is created by SignerFactory + if (!(signConfig.getSigner() instanceof LocalSigner)) { + if (signConfig.getCertificates().isEmpty()) { + throw new CodeSignException("No certificates configured for sign"); + } + } + return SignedDataGenerator.BC.generateSignedData(signedData, signConfig); + } + + /** + * At here, segment header, fsverity info/hap/so info segment, merkle tree + * segment should all be generated. + * code sign block size, segment number, offset is not updated. + * Try to update whatever could be updated here. + * + * @param codeSignBlock CodeSignBlock + */ + private void updateCodeSignBlock(CodeSignBlock codeSignBlock) { + // construct segment header list + codeSignBlock.setSegmentHeaders(); + // Compute and set segment number + codeSignBlock.setSegmentNum(); + // update code sign block header flag + codeSignBlock.setCodeSignBlockFlag(); + // compute segment offset + codeSignBlock.computeSegmentOffset(); + } + + private List parseCentralDirectory(byte[] buffer, int count) { + List cdList = new ArrayList<>(); + ByteBuffer cdBuffer = ByteBuffer.allocate(buffer.length).order(ByteOrder.LITTLE_ENDIAN); + cdBuffer.put(buffer); + cdBuffer.rewind(); + for (int i = 0; i < count; i++) { + byte[] bytesBeforeCompressionMethod = new byte[CentralDirectory.BYTE_SIZE_BEFORE_COMPRESSION_METHOD]; + cdBuffer.get(bytesBeforeCompressionMethod); + char compressionMode = cdBuffer.getChar(); + CentralDirectory.Builder builder = new CentralDirectory.Builder().setCompressionMethod(compressionMode); + byte[] bytesBetweenCmprMethodAndFileNameLength + = new byte[CentralDirectory.BYTE_SIZE_BETWEEN_COMPRESSION_MODE_AND_FILE_SIZE]; + cdBuffer.get(bytesBetweenCmprMethodAndFileNameLength); + char fileNameLength = cdBuffer.getChar(); + char extraFieldLength = cdBuffer.getChar(); + char fileCommentLength = cdBuffer.getChar(); + byte[] attributes + = new byte[CentralDirectory.BYTE_SIZE_BETWEEN_FILE_COMMENT_LENGTH_AND_LOCHDR_RELATIVE_OFFSET]; + cdBuffer.get(attributes); + int locHdrOffset = cdBuffer.getInt(); + builder.setFileNameLength(fileNameLength).setExtraFieldLength(extraFieldLength) + .setFileCommentLength(fileCommentLength).setRelativeOffsetOfLocalHeader(locHdrOffset); + byte[] fileNameBuffer = new byte[fileNameLength]; + cdBuffer.get(fileNameBuffer); + if (extraFieldLength != 0) { + cdBuffer.get(new byte[extraFieldLength]); + } + if (fileCommentLength != 0) { + cdBuffer.get(new byte[fileCommentLength]); + } + CentralDirectory cd = builder.setFileName(fileNameBuffer).build(); + cdList.add(cd); + } + + return cdList; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/SignedDataGenerator.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/SignedDataGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..03d1b2bb4b52d5ccb24ef31c2369e5aa37cd0368 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/SignedDataGenerator.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.sign; + +import com.ohos.hapsigntool.codesigning.exception.CodeSignException; +import com.ohos.hapsigntool.hap.config.SignerConfig; + +/** + * Signed data generator interface + * + * @since 2023/06/05 + */ +@FunctionalInterface +public interface SignedDataGenerator { + /** + * Create a BcSignedDataGenerator instance + */ + SignedDataGenerator BC = new BcSignedDataGenerator(); + + /** + * Generate signature data with specific content and sign configuration. + * + * @param content unsigned file digest content. + * @param signerConfig sign configurations. + * @return signed data. + * @throws CodeSignException if error. + */ + byte[] generateSignedData(byte[] content, SignerConfig signerConfig) throws CodeSignException; +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/VerifyCodeSignature.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/VerifyCodeSignature.java new file mode 100644 index 0000000000000000000000000000000000000000..119a82d4d228ce4ef2407d30454400c7c9954346 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/sign/VerifyCodeSignature.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.sign; + +import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlock; +import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlockHeader; +import com.ohos.hapsigntool.codesigning.datastructure.ElfSignBlock; +import com.ohos.hapsigntool.codesigning.datastructure.Extension; +import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment; +import com.ohos.hapsigntool.codesigning.datastructure.HapInfoSegment; +import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension; +import com.ohos.hapsigntool.codesigning.datastructure.NativeLibInfoSegment; +import com.ohos.hapsigntool.codesigning.datastructure.SegmentHeader; +import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; +import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; +import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator; +import com.ohos.hapsigntool.codesigning.utils.CmsUtils; +import com.ohos.hapsigntool.hap.entity.Pair; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.cms.CMSException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Verify code signature given a file with code sign block + * + * @since 2023/09/08 + */ +public class VerifyCodeSignature { + private static final Logger LOGGER = LogManager.getLogger(VerifyCodeSignature.class); + + private static final String NATIVE_LIB_AN_SUFFIX = ".an"; + + private static final String NATIVE_LIB_SO_SUFFIX = ".so"; + + private static final List EXTRACTED_NATIVE_LIB_SUFFIXS = new ArrayList<>(); + + static { + EXTRACTED_NATIVE_LIB_SUFFIXS.add(NATIVE_LIB_AN_SUFFIX); + EXTRACTED_NATIVE_LIB_SUFFIXS.add(NATIVE_LIB_SO_SUFFIX); + } + + /** + * Verify a signed elf's signature + * + * @param file signed elf file + * @param offset start position of code sign block based on the start of the elf file + * @param length byte size of code sign block + * @param fileFormat elf or hqf or hsp, etc. + * @return true if signature verify succeed and false otherwise + * @throws IOException If an input or output exception occurred + * @throws VerifyCodeSignException parsing result invalid + * @throws FsVerityDigestException if fs-verity digest generation failed + * @throws CMSException if signature verify failed + */ + public static boolean verifyElf(File file, long offset, long length, String fileFormat) + throws IOException, VerifyCodeSignException, FsVerityDigestException, CMSException { + if (!CodeSigning.SUPPORT_BIN_FILE_FORM.contains(fileFormat)) { + LOGGER.info("Not elf file, skip code signing verify"); + return true; + } + // 1) parse sign block to ElfCodeSignBlock object + ElfSignBlock elfSignBlock; + try (FileInputStream signedElf = new FileInputStream(file)) { + byte[] codeSignBlockBytes = new byte[(int) length]; + signedElf.skip(offset); + signedElf.read(codeSignBlockBytes); + elfSignBlock = ElfSignBlock.fromByteArray(codeSignBlockBytes); + } + // 2) verify file data + try (FileInputStream signedElf = new FileInputStream(file)) { + int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset); + byte[] merkleTreeWithPadding = elfSignBlock.getMerkleTreeWithPadding(); + byte[] merkleTree = Arrays.copyOfRange(merkleTreeWithPadding, paddingSize, merkleTreeWithPadding.length); + verifySingleFile(signedElf, elfSignBlock.getDataSize(), elfSignBlock.getSignature(), + elfSignBlock.getTreeOffset(), merkleTree); + } + return true; + } + + /** + * Verify a signed hap's signature + * + * @param file signed hap file + * @param offset start position of code sign block based on the start of the hap file + * @param length byte size of code sign block + * @param fileFormat hap or hqf or hsp, etc. + * @return true if signature verify succeed and false otherwise + * @throws IOException If an input or output exception occurred + * @throws VerifyCodeSignException parsing result invalid + * @throws FsVerityDigestException if fs-verity digest generation failed + * @throws CMSException if signature verify failed + */ + public static boolean verifyHap(File file, long offset, long length, String fileFormat) + throws IOException, VerifyCodeSignException, FsVerityDigestException, CMSException { + if (!CodeSigning.SUPPORT_FILE_FORM.contains(fileFormat)) { + LOGGER.info("Not hap or hsp file, skip code signing verify"); + return true; + } + CodeSignBlock csb = generateCodeSignBlock(file, offset, length); + // 2) verify hap + try (FileInputStream hap = new FileInputStream(file)) { + long dataSize = csb.getHapInfoSegment().getSignInfo().getDataSize(); + byte[] signature = csb.getHapInfoSegment().getSignInfo().getSignature(); + Extension extension = csb.getHapInfoSegment() + .getSignInfo() + .getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED); + MerkleTreeExtension mte = new MerkleTreeExtension(0, 0, null); + if (extension instanceof MerkleTreeExtension) { + mte = (MerkleTreeExtension) extension; + } + // temporary: merkle tree offset set to zero, change to merkleTreeOffset + verifySingleFile(hap, dataSize, signature, mte.getMerkleTreeOffset(), + csb.getOneMerkleTreeByFileName(CodeSigning.HAP_SIGNATURE_ENTRY_NAME)); + } + // 3) verify native libs + try (JarFile inputJar = new JarFile(file, false)) { + for (int i = 0; i < csb.getSoInfoSegment().getSectionNum(); i++) { + String entryName = csb.getSoInfoSegment().getFileNameList().get(i); + byte[] entrySig = csb.getSoInfoSegment().getSignInfoList().get(i).getSignature(); + JarEntry entry = inputJar.getJarEntry(entryName); + if (entry.getSize() != csb.getSoInfoSegment().getSignInfoList().get(i).getDataSize()) { + throw new VerifyCodeSignException( + String.format(Locale.ROOT, "Invalid dataSize of native lib %s", entryName)); + } + InputStream entryInputStream = inputJar.getInputStream(entry); + // temporary merkleTreeOffset 0 + verifySingleFile(entryInputStream, entry.getSize(), entrySig, 0, null); + } + } + return true; + } + + private static CodeSignBlock generateCodeSignBlock(File file, long offset, long length) + throws IOException, VerifyCodeSignException { + CodeSignBlock csb = new CodeSignBlock(); + // 1) parse sign block to CodeSignBlock object + try (FileInputStream signedHap = new FileInputStream(file)) { + int fileReadOffset = 0; + // 0) skip data part, but fileReadOffset remains at start(0) + signedHap.skip(offset); + // 1) parse codeSignBlockHeader + byte[] codeSignBlockHeaderByteArray = new byte[CodeSignBlockHeader.size()]; + fileReadOffset += signedHap.read(codeSignBlockHeaderByteArray); + csb.setCodeSignBlockHeader(CodeSignBlockHeader.fromByteArray(codeSignBlockHeaderByteArray)); + if (csb.getCodeSignBlockHeader().getBlockSize() != length) { + throw new VerifyCodeSignException("Invalid code Sign block size of setCodeSignBlockHeader"); + } + // 2) parse segment headers + for (int i = 0; i < csb.getCodeSignBlockHeader().getSegmentNum(); i++) { + byte[] segmentHeaderByteArray = new byte[SegmentHeader.SEGMENT_HEADER_LENGTH]; + fileReadOffset += signedHap.read(segmentHeaderByteArray); + csb.addToSegmentList(SegmentHeader.fromByteArray(segmentHeaderByteArray)); + } + // compute merkle tree offset by alignment, based on file start + long computedTreeOffset = getAlignmentAddr(CodeSignBlock.PAGE_SIZE_4K, fileReadOffset + offset); + // skip zero padding before merkle tree, adds zero padding length to fileReadOffset + fileReadOffset += signedHap.skip(computedTreeOffset - offset - fileReadOffset); + parseMerkleTree(csb, fileReadOffset, signedHap, computedTreeOffset); + } + return csb; + } + + private static void parseMerkleTree(CodeSignBlock csb, int readOffset, FileInputStream signedHap, + long computedTreeOffset) throws VerifyCodeSignException, IOException { + // check segment offset and segment size + byte[] merkleTreeBytes = new byte[0]; + int fileReadOffset = readOffset; + for (SegmentHeader segmentHeader : csb.getSegmentHeaderList()) { + if (fileReadOffset > segmentHeader.getSegmentOffset()) { + throw new VerifyCodeSignException("Invaild offset of merkle tree and segment header"); + } + // get merkle tree bytes + if (fileReadOffset < segmentHeader.getSegmentOffset()) { + merkleTreeBytes = new byte[segmentHeader.getSegmentOffset() - fileReadOffset]; + fileReadOffset += signedHap.read(merkleTreeBytes); + } + byte[] sh = new byte[segmentHeader.getSegmentSize()]; + fileReadOffset += signedHap.read(sh); + if (segmentHeader.getType() == SegmentHeader.CSB_FSVERITY_INFO_SEG) { + // 3) parse fs-verity info segment + csb.setFsVerityInfoSegment(FsVerityInfoSegment.fromByteArray(sh)); + } else if (segmentHeader.getType() == SegmentHeader.CSB_HAP_META_SEG) { + // 4) parse hap info segment + csb.setHapInfoSegment(HapInfoSegment.fromByteArray(sh)); + } else if (segmentHeader.getType() == SegmentHeader.CSB_NATIVE_LIB_INFO_SEG) { + // 5) parse so info segment + csb.setSoInfoSegment(NativeLibInfoSegment.fromByteArray(sh)); + } + } + if (fileReadOffset != csb.getCodeSignBlockHeader().getBlockSize()) { + throw new VerifyCodeSignException("Invalid blockSize of getCodeSignBlockHeader"); + } + // parse merkle tree + Extension extension = csb.getHapInfoSegment() + .getSignInfo() + .getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED); + if (extension == null) { + throw new VerifyCodeSignException("Missing merkleTreeExtension in verifycation"); + } + if (extension instanceof MerkleTreeExtension) { + MerkleTreeExtension mte = (MerkleTreeExtension) extension; + if (computedTreeOffset != mte.getMerkleTreeOffset()) { + throw new VerifyCodeSignException("Invalid merkle tree offset"); + } + if (merkleTreeBytes.length != mte.getMerkleTreeSize()) { + throw new VerifyCodeSignException("Invalid merkle tree size"); + } + csb.addOneMerkleTree(CodeSigning.HAP_SIGNATURE_ENTRY_NAME, merkleTreeBytes); + } + } + + private static long getAlignmentAddr(long alignment, long input) { + long residual = input % alignment; + if (residual == 0) { + return input; + } else { + return input + (alignment - residual); + } + } + + /** + * Verifies the signature of a given file with its signature in pkcs#7 format + * + * @param input file being verified in InputStream representation + * @param length size of signed data in the file + * @param signature byte array of signature in pkcs#7 format + * @param merkleTreeOffset merkle tree offset based on file start + * @param inMerkleTreeBytes merkle tree raw bytes + * @throws FsVerityDigestException if fs-verity digest generation failed + * @throws CMSException if signature verify failed + * @throws VerifyCodeSignException parsing code sign block error + */ + public static void verifySingleFile(InputStream input, long length, byte[] signature, long merkleTreeOffset, + byte[] inMerkleTreeBytes) throws FsVerityDigestException, CMSException, VerifyCodeSignException { + Pair pairResult = generateFsVerityDigest(input, length, merkleTreeOffset); + byte[] generatedMerkleTreeBytes = pairResult.getSecond(); + if (generatedMerkleTreeBytes == null) { + generatedMerkleTreeBytes = new byte[0]; + } + // For native libs, inMerkleTreeBytes is null, skip check here + if ((inMerkleTreeBytes != null) && !Arrays.equals(inMerkleTreeBytes, generatedMerkleTreeBytes)) { + throw new VerifyCodeSignException("verify merkle tree bytes failed"); + } + CmsUtils.verifySignDataWithUnsignedDataDigest(pairResult.getFirst(), signature); + } + + private static Pair generateFsVerityDigest(InputStream inputStream, long size, + long merkleTreeOffset) throws FsVerityDigestException { + FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); + fsVerityGenerator.generateFsVerityDigest(inputStream, size, merkleTreeOffset); + return Pair.create(fsVerityGenerator.getFsVerityDigest(), fsVerityGenerator.getTreeBytes()); + } + + /** + * Get entry name of all native files in hap + * + * @param hap the given hap + * @return list of entry name + */ + private static List getNativeEntriesFromHap(JarFile hap) { + List result = new ArrayList<>(); + for (Enumeration e = hap.entries(); e.hasMoreElements();) { + JarEntry entry = e.nextElement(); + if (!entry.isDirectory()) { + if (!isNativeFile(entry.getName())) { + continue; + } + result.add(entry.getName()); + } + } + return result; + } + + /** + * Check whether the entry is a native file + * + * @param entryName the name of entry + * @return true if it is a native file, and false otherwise + */ + private static boolean isNativeFile(String entryName) { + for (String suffix : EXTRACTED_NATIVE_LIB_SUFFIXS) { + if (entryName.endsWith(suffix)) { + return true; + } + } + return false; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/CmsUtils.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/CmsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..df4b11a7d18f1de22400abd3c26d36f17946ec53 --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/CmsUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.utils; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; + +import java.security.Security; +import java.security.cert.CertificateException; +import java.util.Collection; + +/** + * CMS utils class + * + * @since 2023/06/05 + */ +public class CmsUtils { + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + /** + * Private constructor + */ + private CmsUtils() { + } + + private static void isCollectionValid(Collection collection) + throws OperatorCreationException { + if (collection == null) { + throw new OperatorCreationException("No matched cert: " + collection); + } + if (collection.size() != 1) { + throw new OperatorCreationException( + "More than one matched certs, matched certs size: " + collection.size()); + } + } + + @SuppressWarnings("unchecked") + private static boolean verifyCmsSignedData(CMSSignedData cmsSignedData) throws CMSException { + return cmsSignedData.verifySignatures(signId -> { + Collection collection = cmsSignedData.getCertificates().getMatches(signId); + isCollectionValid(collection); + X509CertificateHolder cert = collection.iterator().next(); + try { + return new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert); + } catch (CertificateException e) { + throw new OperatorCreationException("Verify BC signatures failed: " + e.getMessage(), e); + } + }); + } + + /** + * Verify signed data using an unsigned data digest + * + * @param unsignedDataDigest unsigned data digest + * @param signedData signed data + * @return true if verify success + * @throws CMSException if error + */ + public static boolean verifySignDataWithUnsignedDataDigest(byte[] unsignedDataDigest, byte[] signedData) + throws CMSException { + CMSSignedData cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(unsignedDataDigest), signedData); + return verifyCmsSignedData(cmsSignedData); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/DigestUtils.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/DigestUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..aafbaf577149d08d65b1f4052d54ed1e5836bd0b --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/DigestUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Digest util class + * + * @since 2023/06/05 + */ +public class DigestUtils { + /** + * digest the inputContent with specific algorithm + * + * @param inputContentArray input Content Array + * @param algorithm hash algorithm + * @return the result of digest, is a byte array + * @throws NoSuchAlgorithmException if error + */ + public static byte[] computeDigest(byte[] inputContentArray, String algorithm) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance(algorithm); + md.update(inputContentArray); + return md.digest(); + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/HapUtils.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/HapUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d3c9ab4f57926de83038c014a43404c786791a0c --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/HapUtils.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.utils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * utility for check hap configs + * + * @since 2023/06/05 + */ +public class HapUtils { + private static final String COMPRESS_NATIVE_LIBS_OPTION = "compressNativeLibs"; + + private static final List HAP_CONFIG_FILES = new ArrayList<>(); + + private static final String HAP_FA_CONFIG_JSON_FILE = "config.json"; + + private static final String HAP_STAGE_MODULE_JSON_FILE = "module.json"; + + static { + HAP_CONFIG_FILES.add(HAP_FA_CONFIG_JSON_FILE); + HAP_CONFIG_FILES.add(HAP_STAGE_MODULE_JSON_FILE); + } + + private HapUtils() { + } + + /** + * Check configuration in hap to find out whether the native libs are compressed + * + * @param hapFile the given hap + * @return boolean value of parsing result + * @throws IOException io error + */ + public static boolean checkCompressNativeLibs(File hapFile) throws IOException { + try (JarFile inputJar = new JarFile(hapFile, false)) { + for (String configFile : HAP_CONFIG_FILES) { + JarEntry entry = inputJar.getJarEntry(configFile); + if (entry == null) { + continue; + } + try (InputStream data = inputJar.getInputStream(entry)) { + String jsonString = new String(InputStreamUtils.toByteArray(data, (int) entry.getSize()), + StandardCharsets.UTF_8); + return checkCompressNativeLibs(jsonString); + } + } + } + return true; + } + + /** + * Check whether the native libs are compressed by parsing config json + * + * @param jsonString the config json string + * @return boolean value of parsing result + */ + public static boolean checkCompressNativeLibs(String jsonString) { + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + Queue queue = new LinkedList<>(); + queue.offer(jsonObject); + while (queue.size() > 0) { + JsonObject curJsonObject = queue.poll(); + JsonElement jsonElement = curJsonObject.get(COMPRESS_NATIVE_LIBS_OPTION); + if (jsonElement != null) { + return jsonElement.getAsBoolean(); + } + for (Map.Entry entry : curJsonObject.entrySet()) { + if (entry.getValue().isJsonObject()) { + queue.offer(entry.getValue().getAsJsonObject()); + } + } + } + // default to compress native libs + return true; + } +} diff --git a/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/InputStreamUtils.java b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/InputStreamUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..816ea38c1138737404e0bdadab2e196c5b8d3cfd --- /dev/null +++ b/hapsigntool/hap_sign_tool_lib/src/main/java/com/ohos/hapsigntool/codesigning/utils/InputStreamUtils.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023-2023 Huawei Device Co., Ltd. + * 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.ohos.hapsigntool.codesigning.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * InputStream util class + * + * @since 2023/08/10 + */ +public class InputStreamUtils { + private static final int BUFFER_SIZE = 4096; + + /** + * get byte array by inputStream and size + * + * @param inputStream inputStream data + * @param inputStreamSize inputStream size + * @return byte array value + * @throws IOException io error + */ + public static byte[] toByteArray(InputStream inputStream, int inputStreamSize) throws IOException { + if (inputStreamSize == 0) { + return new byte[0]; + } + if (inputStreamSize < 0) { + throw new IllegalArgumentException("inputStreamSize: " + inputStreamSize + "is less than zero: "); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(inputStream, inputStreamSize, output); + return output.toByteArray(); + } + + private static int copy(InputStream inputStream, int inputStreamSize, OutputStream output) throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int readSize = 0; + int count = 0; + while (readSize < inputStreamSize && (readSize = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, readSize); + count += readSize; + } + if (count != inputStreamSize) { + throw new IOException("read size err. readSizeCount: " + count + ", inputStreamSize: " + inputStreamSize); + } + return count; + } +}