package signingblock

import (
	"bytes"
	"crypto/sha256"
	"crypto/x509"
	"encoding/binary"
	"errors"
	"fmt"
	"hash"
	"io"
	"math"
	"os"

	"github.com/avast/apkparser"

	"github.com/avast/apkverifier/apilevel"
)

// https://source.android.com/security/apksigning/v2.html
// frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java

type BlockId uint32

const (
	// BlockIdDependencyMetadata Dependencies metadata generated by Gradle and encrypted by Google Play.
	// "...The data is compressed, encrypted by a Google Play signing key..."
	// https://developer.android.com/studio/releases/gradle-plugin#dependency-metadata
	BlockIdDependencyMetadata BlockId = 0x504b4453

	// BlockIdMeituanMetadata JSON with some metadata, used by Chinese company Meituan
	BlockIdMeituanMetadata BlockId = 0x71777777

	// BlockIdSourceStampV1 Older SourceStamp implementation, you should not encounter this ID
	// https://android.googlesource.com/platform/frameworks/base/+/549ce7a482ed4fe170ca445324fb38c447030404%5E%21/#F0
	BlockIdSourceStampV1 BlockId = 0x2b09189e

	blockIdVerityPadding BlockId = 0x42726577
	blockIdSchemeV2      BlockId = 0x7109871a
	blockIdSchemeV3      BlockId = 0xf05368c0
	blockIdSchemeV31     BlockId = 0x1b93ad61
	blockIdFrosting      BlockId = 0x2146444e
	blockIdSourceStampV2 BlockId = 0x6dff800d
)

func (b BlockId) String() string {
	switch b {
	case BlockIdDependencyMetadata:
		return "DependencyMetadata"
	case BlockIdMeituanMetadata:
		return "MeituanMetadata"
	case BlockIdSourceStampV1:
		return "SourceStampV1 (deprecated)"
	case blockIdSourceStampV2:
		return "SourceStampV2"
	case blockIdVerityPadding:
		return "VerityPadding"
	case blockIdSchemeV2:
		return "SchemeV2Signature"
	case blockIdSchemeV3:
		return "SchemeV3Signature"
	case blockIdSchemeV31:
		return "SchemeV3.1Signature"
	case blockIdFrosting:
		return "PlayFrosting"
	default:
		return fmt.Sprintf("Unknown (0x%08x)", uint32(b))
	}
}

const (
	eocdRecMinSize             = 22
	eocdRecMagic               = 0x06054b50
	eocdCommentSizeOffset      = 20
	eocdCentralDirSizeOffset   = 12
	eocdCentralDirOffsetOffset = 16

	zip64LocatorSize  = 20
	zip64LocatorMagic = 0x07064b50

	apkSigBlockMinSize = 32
	apkSigBlockMagicHi = 0x3234206b636f6c42
	apkSigBlockMagicLo = 0x20676953204b5041

	maxChunkSize = 1024 * 1024

	maxApkSigners = 10

	schemeIdV1  = 1
	schemeIdV2  = 2
	schemeIdV3  = 3
	schemeIdV31 = 31
)

var schemeIdToBlockId = map[int32]BlockId{
	schemeIdV2:  blockIdSchemeV2,
	schemeIdV3:  blockIdSchemeV3,
	schemeIdV31: blockIdSchemeV31,
}

var (
	errNoSigningBlockSignature = errors.New("This apk does not have signing block signature")
	errEocdNotFound            = errors.New("EOCD record not found.")
)

type signatureBlockScheme interface {
	parseSigners(block *bytes.Buffer, contentDigests map[contentDigest][]byte, result *VerificationResult)
	finalizeResult(minSdkVersion, maxSdkVersion int32, result *VerificationResult)
}

type signingBlock struct {
	file             io.ReadSeeker
	fileSize         int64
	eocdOffset       int64
	centralDirOffset int64
	centralDirSize   int64
	sigBlockOffset   int64
	eocd             []byte
}

type signingBlockNotFoundError struct {
	err error
}

func (e *signingBlockNotFoundError) Error() string {
	return "Signature Block signature not found: " + e.err.Error()
}

func IsSigningBlockNotFoundError(err error) bool {
	_, ok := err.(*signingBlockNotFoundError)
	return ok
}

func VerifySigningBlock(path string, minSdkVersion, maxSdkVersion int32) (res *VerificationResult, magic uint32, err error) {
	f, err := os.Open(path)
	if err != nil {
		return
	}
	defer f.Close()
	return VerifySigningBlockReader(f, minSdkVersion, maxSdkVersion)
}

func VerifySigningBlockReaderWithZip(r io.ReadSeeker, minSdkVersion, maxSdkVersion int32, optionalZip *apkparser.ZipReader) (res *VerificationResult, magic uint32, err error) {
	if optionalZip == nil {
		optionalZip, err = apkparser.OpenZipReader(r)
		if err != nil {
			return
		}
		defer optionalZip.Close()
	}

	var s *signingBlock
	s, magic, err = newSigningBlock(r)
	if err != nil {
		return
	}

	res = &VerificationResult{}
	blocks, frosting, err := s.findSignatureBlocks(maxSdkVersion, res)
	if frosting != nil && res.Frosting.Error == nil {
		v2block := blocks[blockIdSchemeV2]
		if v2block == nil && len(res.ExtraBlocks) != 0 {
			v2block = res.ExtraBlocks[blockIdSchemeV2]
		}
		res.Frosting.Error = frosting.verifyApk(r, s.sigBlockOffset, v2block, s.centralDirOffset, s.centralDirSize, s.eocd)
	}
	if err != nil {
		err = &signingBlockNotFoundError{err}
		return
	}

	contentDigests := s.pickAndVerify(blocks, minSdkVersion, maxSdkVersion, res)
	// On levels < 28, we can fallback to v2 - v3 is not required to be valid.
	// 1e2ab0af91dce1be16525d9d6d6e6d645788ea627edc64cb9cd379b35e01f53f
	if res.ContainsErrors() && res.SchemeId >= schemeIdV3 && minSdkVersion < apilevel.V9_0_Pie && blocks[blockIdSchemeV2] != nil {
		if res.ExtraBlocks == nil {
			res.ExtraBlocks = make(map[BlockId][]byte)
		}
		res.ExtraBlocks[blockIdSchemeV3] = blocks[blockIdSchemeV3]
		delete(blocks, blockIdSchemeV3)

		for _, e := range res.Errors {
			res.Warnings = append(res.Warnings, fmt.Sprintf("schemeV3: %s", e.Error()))
		}
		res.Errors = nil
		contentDigests = s.pickAndVerify(blocks, minSdkVersion, maxSdkVersion, res)
	}

	stampVerifier := sourceStampVerifier{
		verifiedSchemeId: int32(res.SchemeId),
		minSdkVersion:    minSdkVersion,
		maxSdkVersion:    maxSdkVersion,
	}

	res.SourceStamp = stampVerifier.VerifySourceV2Stamp(optionalZip, blocks[blockIdSourceStampV2], contentDigests)

	err = res.GetLastError()
	return
}

func (s *signingBlock) pickAndVerify(blocks map[BlockId][]byte, minSdkVersion, maxSdkVersion int32, res *VerificationResult) map[contentDigest][]byte {
	if !apilevel.SupportsSigV2(maxSdkVersion) {
		res.SchemeId = 1
		return nil
	}

	var block []byte
	var err error
	var scheme signatureBlockScheme
	res.SchemeId, scheme, block, err = s.pickScheme(blocks, minSdkVersion, maxSdkVersion)
	if err != nil {
		res.Errors = append(res.Errors, err)
		return nil
	}

	return s.verify(scheme, block, minSdkVersion, maxSdkVersion, res)
}

func VerifySigningBlockReader(r io.ReadSeeker, minSdkVersion, maxSdkVersion int32) (res *VerificationResult, magic uint32, err error) {
	return VerifySigningBlockReaderWithZip(r, minSdkVersion, maxSdkVersion, nil)
}

func ExtractCerts(path string, minSdkVersion, maxSdkVersion int32) (certs [][]*x509.Certificate, err error) {
	f, err := os.Open(path)
	if err != nil {
		return
	}
	defer f.Close()
	return ExtractCertsReader(f, minSdkVersion, maxSdkVersion)
}

func ExtractCertsReader(r io.ReadSeeker, minSdkVersion, maxSdkVersion int32) (certs [][]*x509.Certificate, err error) {
	var s *signingBlock
	s, _, err = newSigningBlock(r)
	if err != nil {
		return
	}

	res := &VerificationResult{}
	blocks, _, err := s.findSignatureBlocks(maxSdkVersion, res)
	if err != nil {
		err = &signingBlockNotFoundError{err}
		return
	}

	var block []byte
	var scheme signatureBlockScheme
	res.SchemeId, scheme, block, err = s.pickScheme(blocks, minSdkVersion, maxSdkVersion)
	if err != nil {
		return nil, err
	}

	s.extractCerts(scheme, block, res)
	return res.Certs, res.GetLastError()
}

func newSigningBlock(r io.ReadSeeker) (sblock *signingBlock, magic uint32, err error) {
	size, err := r.Seek(0, io.SeekEnd)
	if err != nil {
		err = fmt.Errorf("failed to seek to the end of the APK file: %s", err.Error())
	}

	if _, err := r.Seek(0, io.SeekStart); err != nil {
		err = fmt.Errorf("failed to seek to the start of the APK file: %s", err.Error())
	}

	if size < 4 {
		err = fmt.Errorf("APK file is too short (%d bytes).", size)
		return
	}

	if err = binary.Read(r, binary.LittleEndian, &magic); err != nil {
		err = fmt.Errorf("Failed to read APK magic: %s", err.Error())
		return
	}

	sblock = &signingBlock{
		file:     r,
		fileSize: size,
	}
	return
}

func (s *signingBlock) findEocd() error {
	if s.fileSize < eocdRecMinSize {
		return fmt.Errorf("APK file is too short (%d bytes).", s.fileSize)
	}

	if err := s.findEocdMaxCommentSize(0); err == nil {
		return nil
	}
	return s.findEocdMaxCommentSize(math.MaxUint16)
}

func (s *signingBlock) findEocdMaxCommentSize(maxCommentSize int) error {
	if maxCommentSize > int(s.fileSize-eocdRecMinSize) {
		maxCommentSize = int(s.fileSize - eocdRecMinSize)
	}

	buf := make([]byte, eocdRecMinSize+maxCommentSize)
	bufOffsetInFile := s.fileSize - int64(len(buf))

	if _, err := s.file.Seek(bufOffsetInFile, io.SeekStart); err != nil {
		return err
	}

	if _, err := io.ReadFull(s.file, buf); err != nil {
		return err
	}

	maxCommentSize = len(buf) - eocdRecMinSize
	if maxCommentSize > math.MaxUint16 {
		maxCommentSize = math.MaxUint16
	}
	emptyCommentStart := len(buf) - eocdRecMinSize

	for commentSize := 0; commentSize <= maxCommentSize; commentSize++ {
		pos := emptyCommentStart - commentSize
		if binary.LittleEndian.Uint32(buf[pos:pos+4]) == eocdRecMagic {
			recordCommentSize := binary.LittleEndian.Uint16(buf[pos+eocdCommentSizeOffset:])
			if int(recordCommentSize) == commentSize {
				s.eocdOffset = bufOffsetInFile + int64(pos)
				s.centralDirOffset = int64(binary.LittleEndian.Uint32(buf[pos+eocdCentralDirOffsetOffset:]))
				s.eocd = buf[pos:]

				if s.centralDirOffset >= s.eocdOffset {
					return fmt.Errorf("ZIP Central Directory offset ouf of range: %d. Zip End of Central Directory offset: %d",
						s.centralDirOffset, s.eocdOffset)
				}

				s.centralDirSize = int64(binary.LittleEndian.Uint32(buf[pos+eocdCentralDirSizeOffset:]))
				if s.centralDirOffset+int64(s.centralDirSize) != s.eocdOffset {
					return errors.New("ZIP Central Directory is not immediately followed by End of Central Directory")
				}
				return nil
			}
		}
	}
	return errEocdNotFound
}

func (s *signingBlock) isZip64() bool {
	locatorPos := s.eocdOffset - zip64LocatorSize
	if locatorPos < 0 {
		return false
	}

	if _, err := s.file.Seek(locatorPos, io.SeekStart); err != nil {
		return false
	}

	var magic uint32
	if err := binary.Read(s.file, binary.LittleEndian, &magic); err != nil {
		return false
	}
	return magic == zip64LocatorMagic
}

func (s *signingBlock) pickScheme(blocks map[BlockId][]byte, minSdkVersion, maxSdkVersion int32) (schemeId int, scheme signatureBlockScheme, block []byte, err error) {
	if block = blocks[blockIdSchemeV31]; block != nil {
		schemeId = schemeIdV31
		scheme = &schemeV31{
			backendV31: schemeV3{
				actingAsV31:   true,
				minSdkVersion: minSdkVersion,
				maxSdkVersion: maxSdkVersion,
			},
			backendV3: schemeV3{
				minSdkVersion: minSdkVersion,
				maxSdkVersion: maxSdkVersion,
			},
		}
	} else if block = blocks[blockIdSchemeV3]; block != nil {
		schemeId = schemeIdV3
		scheme = &schemeV3{minSdkVersion: minSdkVersion, maxSdkVersion: maxSdkVersion}
	} else if block = blocks[blockIdSchemeV2]; block != nil {
		schemeId = schemeIdV2
		scheme = &schemeV2{minSdkVersion, maxSdkVersion}
	} else {
		schemeId = schemeIdV1
		err = &signingBlockNotFoundError{errors.New("No APK Signature block in APK Signing Block")}
	}
	return
}

func (s *signingBlock) findApkSigningBlock() (block []byte, offset int64, err error) {
	if s.centralDirOffset < apkSigBlockMinSize {
		err = errNoSigningBlockSignature
		return
	}

	footer := make([]byte, 24)

	if _, err = s.file.Seek(s.centralDirOffset-int64(len(footer)), io.SeekStart); err != nil {
		return
	}

	if _, err = io.ReadFull(s.file, footer); err != nil {
		return
	}

	if binary.LittleEndian.Uint64(footer[8:]) != apkSigBlockMagicLo ||
		binary.LittleEndian.Uint64(footer[16:]) != apkSigBlockMagicHi {
		err = errNoSigningBlockSignature
		return
	}

	blockSizeFooter := binary.LittleEndian.Uint64(footer)
	if blockSizeFooter < uint64(len(footer)) || blockSizeFooter > math.MaxInt32-8 {
		err = fmt.Errorf("APK Signing Block size out of range: %d", blockSizeFooter)
		return
	}

	totalSize := int64(blockSizeFooter + 8)
	if totalSize < apkSigBlockMinSize {
		err = fmt.Errorf("Apk Signing Block is too small: %d vs %d", totalSize, apkSigBlockMinSize)
		return
	}

	offset = int64(s.centralDirOffset) - totalSize
	if offset < 0 {
		err = fmt.Errorf("APK Signing Block offset out of range: %d", offset)
		return
	}

	block = make([]byte, totalSize)
	if _, err = s.file.Seek(offset, io.SeekStart); err != nil {
		return
	}

	if _, err = io.ReadFull(s.file, block); err != nil {
		return
	}

	if blockSizeHeader := binary.LittleEndian.Uint64(block); blockSizeHeader != blockSizeFooter {
		err = fmt.Errorf("APK Signing Block sizes in header and footer do not match: %d vs %d",
			blockSizeHeader, blockSizeFooter)
		return
	}

	return
}

func (s *signingBlock) findSignatureBlocks(maxSdkVersion int32, res *VerificationResult) (blocks map[BlockId][]byte, frostingRes *frostingInfo, err error) {
	if err = s.findEocd(); err != nil {
		return
	}

	if s.isZip64() {
		err = errors.New("ZIP64 APK not supported")
		return
	}

	var sigBlock []byte
	sigBlock, s.sigBlockOffset, err = s.findApkSigningBlock()
	if err != nil {
		return
	}

	pairs := bytes.NewReader(sigBlock[8 : len(sigBlock)-24])
	entryCount := 0

	blocks = make(map[BlockId][]byte)
	for pairs.Len() > 0 {
		entryCount++

		if pairs.Len() < 8 {
			err = fmt.Errorf("Insufficient data to read size of APK Signing Block entry #%d", entryCount)
			return
		}

		var entryLen int64
		binary.Read(pairs, binary.LittleEndian, &entryLen)
		if entryLen < 4 || entryLen > math.MaxInt32 {
			err = fmt.Errorf("APK Signing Block entry #%d size out of range: %d", entryCount, entryLen)
			return
		}

		nextEntryPos := pairs.Size() - int64(pairs.Len()) + entryLen
		if entryLen > int64(pairs.Len()) {
			err = fmt.Errorf("APK Signing Block entry #%d size out of range: %d, available: %d",
				entryCount, entryLen, pairs.Len())
			return
		}

		var id uint32
		if err = binary.Read(pairs, binary.LittleEndian, &id); err != nil {
			err = fmt.Errorf("failed to read signing block id: %s", err.Error())
			return
		}

		switch bid := BlockId(id); bid {
		case blockIdSchemeV2, blockIdSchemeV3, blockIdSchemeV31:
			block := make([]byte, entryLen-4)
			if _, err = pairs.Read(block); err != nil {
				return
			}

			isSupported := (bid == blockIdSchemeV2 && apilevel.SupportsSigV2(maxSdkVersion)) ||
				(bid == blockIdSchemeV3 && apilevel.SupportsSigV3(maxSdkVersion)) ||
				(bid == blockIdSchemeV31 && apilevel.SupportsSigV31(maxSdkVersion))

			if isSupported {
				blocks[bid] = block
			}

			if !isSupported || (bid == blockIdSchemeV3 && apilevel.SupportsSigV31(maxSdkVersion)) {
				if res.ExtraBlocks == nil {
					res.ExtraBlocks = make(map[BlockId][]byte)
				}
				res.ExtraBlocks[bid] = block
			}
		case blockIdVerityPadding:
			// Block full of zeros to ensure the signing block is padded to 4K,
			// for verity signature verification.
		case blockIdFrosting:
			frostingBlock := make([]byte, entryLen-4)
			if _, err = pairs.Read(frostingBlock); err != nil {
				return
			}

			var frosting frostingInfo
			var frResult FrostingResult
			res.Frosting = &frResult
			frResult.KeySha256, frResult.ProtobufInfo, frResult.Error = frosting.parse(frostingBlock)
			if frResult.Error != nil {
				res.addWarning("frosting: %s", frResult.Error.Error())
			} else {
				frostingRes = &frosting
			}
		case blockIdSourceStampV2:
			block := make([]byte, entryLen-4)
			if _, err = pairs.Read(block); err != nil {
				return
			}
			blocks[bid] = block
		default:
			block := make([]byte, entryLen-4)
			if _, err = pairs.Read(block); err != nil {
				return
			}
			if res.ExtraBlocks == nil {
				res.ExtraBlocks = make(map[BlockId][]byte)
			}
			res.ExtraBlocks[bid] = block
		}

		if _, err = pairs.Seek(nextEntryPos, io.SeekStart); err != nil {
			return
		}
	}

	return
}

func (s *signingBlock) extractCerts(scheme signatureBlockScheme, block []byte, res *VerificationResult) map[contentDigest][]byte {
	contentDigests := make(map[contentDigest][]byte)
	signatureBlock := bytes.NewBuffer(block)

	scheme.parseSigners(signatureBlock, contentDigests, res)
	return contentDigests
}

func (s *signingBlock) verify(scheme signatureBlockScheme, block []byte, minSdkVersion, maxSdkVersion int32, res *VerificationResult) map[contentDigest][]byte {
	contentDigests := s.extractCerts(scheme, block, res)
	if len(res.Certs) == 0 {
		res.addError("no signers found")
		return nil
	}

	if len(contentDigests) == 0 {
		res.addError("no content digests found")
		return nil
	}

	if !s.verifyIntegrity(contentDigests, res) {
		return contentDigests
	}

	scheme.finalizeResult(minSdkVersion, maxSdkVersion, res)
	return contentDigests
}

func (s *signingBlock) verifyIntegrity(expectedDigests map[contentDigest][]byte, result *VerificationResult) bool {
	beforeApkSigningBlock := &dataSourceApk{file: s.file, start: 0, end: s.sigBlockOffset}
	centralDir := &dataSourceApk{file: s.file, start: s.centralDirOffset, end: s.eocdOffset}
	eocd := &dataSourceBytes{data: append([]byte(nil), s.eocd...)}

	// For the purposes of integrity verification, ZIP End of Central Directory's field Start of
	// Central Directory must be considered to point to the offset of the APK Signing Block.
	binary.LittleEndian.PutUint32(eocd.data[eocdCentralDirOffsetOffset:], uint32(s.sigBlockOffset))

	digestAlgorithms := make([]contentDigest, 0, len(expectedDigests))
	for algo := range expectedDigests {
		digestAlgorithms = append(digestAlgorithms, algo)
	}

	actualDigests, err := s.computeContentDigests(digestAlgorithms, beforeApkSigningBlock, centralDir, eocd)
	if err != nil {
		result.addError("Failed to compute digest(s) of contents: %s", err.Error())
		return false
	}

	for _, algo := range digestAlgorithms {
		if !bytes.Equal(expectedDigests[algo], actualDigests[algo]) {
			result.addError("%d digest of contents did not verify.", algo)
			continue

		}
	}
	return true
}

func (s *signingBlock) computeContentDigests(digestAlgorithms []contentDigest, contents ...dataSource) (map[contentDigest][]byte, error) {
	var oneMbAlgos []contentDigest
	hasVerity := false
	for _, algo := range digestAlgorithms {
		switch algo {
		case digestChunkedSha256, digestChunkedSha512:
			oneMbAlgos = append(oneMbAlgos, algo)
		case digestVeritySha256:
			hasVerity = true
		default:
			panic(fmt.Sprintf("unhandled crypto algo %d", int(algo)))
		}
	}

	var result map[contentDigest][]byte
	var err error
	if len(oneMbAlgos) != 0 {
		result, err = s.computeOneMbChunkContentDigests(oneMbAlgos, contents)
		if err != nil {
			return result, err
		}
	}

	if hasVerity {
		if result == nil {
			result = make(map[contentDigest][]byte)
		}
		result[digestVeritySha256], err = s.computeVerityContentDigest(contents)
		if err != nil {
			return result, err
		}
	}

	if len(result) == 0 {
		return nil, fmt.Errorf("no valid digest algorithms")
	}

	return result, nil
}

func (s *signingBlock) computeOneMbChunkContentDigests(digestAlgorithms []contentDigest, contents []dataSource) (map[contentDigest][]byte, error) {
	var totalChunkCount int64
	for _, input := range contents {
		totalChunkCount += input.chunkCount()
	}

	if totalChunkCount >= math.MaxInt32/1024 {
		return nil, fmt.Errorf("Too many chunks: %d", totalChunkCount)
	}

	digestsOfChunks := make([][]byte, len(digestAlgorithms))
	hashers := make([]hash.Hash, len(digestAlgorithms))
	for i, algo := range digestAlgorithms {
		buf := make([]byte, 5+totalChunkCount*int64(algo.Hash().Size()))
		buf[0] = 0x5a
		binary.LittleEndian.PutUint32(buf[1:], uint32(totalChunkCount))

		digestsOfChunks[i] = buf
		hashers[i] = algo.Hash().New()
	}

	chunkContentPrefix := make([]byte, 5)
	chunkContentPrefix[0] = 0xa5

	chunkIndex := 0
	for inputIdx, input := range contents {
		var offset int64
		remaining := input.length()
		for remaining > 0 {
			chunkSize := remaining
			if chunkSize > maxChunkSize {
				chunkSize = maxChunkSize
			}

			binary.LittleEndian.PutUint32(chunkContentPrefix[1:], uint32(chunkSize))

			for i := range hashers {
				hashers[i].Write(chunkContentPrefix)

				if err := input.writeTo(hashers[i], offset, chunkSize); err != nil {
					return nil, fmt.Errorf("Failed to digest chunk #%d of section #%d", chunkIndex, inputIdx)
				}

				sum := hashers[i].Sum(nil)
				hashers[i].Reset()

				copy(digestsOfChunks[i][5+chunkIndex*len(sum):], sum)
			}
			offset += chunkSize
			remaining -= chunkSize
			chunkIndex++
		}
	}
	result := make(map[contentDigest][]byte, len(digestAlgorithms))
	for i := range digestsOfChunks {
		hashers[i].Write(digestsOfChunks[i])
		result[digestAlgorithms[i]] = hashers[i].Sum(nil)
	}
	return result, nil
}

func (s *signingBlock) computeVerityContentDigest(contents []dataSource) ([]byte, error) {
	if contents[0].length()%verityChunkSize != 0 {
		return nil, fmt.Errorf("APK Signing block size not a multiple of %d", verityChunkSize)
	}

	chained := newChainedDataSource(contents...)
	builder := newVerityTreeBuilder(make([]byte, 8))
	rootHash, err := builder.generateTreeRootHash(chained)
	if err != nil {
		return nil, fmt.Errorf("failed to generate verity tree root hash: %s", err.Error())
	}

	result := make([]byte, sha256.Size+8)
	copy(result, rootHash)
	binary.LittleEndian.PutUint64(result[sha256.Size:], uint64(chained.length()))
	return result, nil
}
