# SPDX-License-Identifier: GPL-2.0+
# Copyright (c) 2022 Softathome
# Written by Philippe Reynes <philippe.reynes@softathome.com>
#
# Entry-type for the global header
#

import os
import struct
from dtoc import fdt_util
from u_boot_pylib import tools

from binman.entry import Entry
from binman.etype.collection import Entry_collection
from binman.entry import EntryArg

from Cryptodome.Hash import SHA256, SHA384, SHA512
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import pkcs1_15
from Cryptodome.Signature import pss

PRE_LOAD_MAGIC = b'UBSH'

RSAS = {
    'rsa1024': 1024 / 8,
    'rsa2048': 2048 / 8,
    'rsa4096': 4096 / 8
}

SHAS = {
    'sha256': SHA256,
    'sha384': SHA384,
    'sha512': SHA512
}

class Entry_pre_load(Entry_collection):
    """Pre load image header

    Properties / Entry arguments:
        - pre-load-key-path: Path of the directory that store key (provided by
          the environment variable PRE_LOAD_KEY_PATH)
        - content: List of phandles to entries to sign
        - algo-name: Hash and signature algo to use for the signature
        - padding-name: Name of the padding (pkcs-1.5 or pss)
        - key-name: Filename of the private key to sign
        - header-size: Total size of the header
        - version: Version of the header

    This entry creates a pre-load header that contains a global
    image signature.

    For example, this creates an image with a pre-load header and a binary::

        binman {
            image2 {
                filename = "sandbox.bin";

                pre-load {
                    content = <&image>;
                    algo-name = "sha256,rsa2048";
                    padding-name = "pss";
                    key-name = "private.pem";
                    header-size = <4096>;
                    version = <1>;
                };

                image: blob-ext {
                    filename = "sandbox.itb";
                };
            };
        };
    """

    def __init__(self, section, etype, node):
        super().__init__(section, etype, node)
        self.algo_name = fdt_util.GetString(self._node, 'algo-name')
        self.padding_name = fdt_util.GetString(self._node, 'padding-name')
        self.key_name = fdt_util.GetString(self._node, 'key-name')
        self.header_size = fdt_util.GetInt(self._node, 'header-size')
        self.version = fdt_util.GetInt(self._node, 'version')

    def ReadNode(self):
        super().ReadNode()
        self.key_path, = self.GetEntryArgsOrProps([EntryArg('pre-load-key-path', str)])
        if self.key_path is None:
            self.key_path = ''

    def _CreateHeader(self):
        """Create a pre load header"""
        hash_name, sign_name = self.algo_name.split(',')
        padding_name = self.padding_name
        key_name = os.path.join(self.key_path, self.key_name)

        # Check hash and signature name/type
        if hash_name not in SHAS:
            self.Raise(hash_name + " is not supported")
        if sign_name not in RSAS:
            self.Raise(sign_name + " is not supported")

        # Read the key
        with open(key_name, 'rb') as pem:
            key = RSA.import_key(pem.read())

        # Check if the key has the expected size
        if key.size_in_bytes() != RSAS[sign_name]:
            self.Raise("The key " + self.key_name + " don't have the expected size")

        # Compute the hash
        hash_image = SHAS[hash_name].new()
        hash_image.update(self.image)

        # Compute the signature
        if padding_name is None:
            padding_name = "pkcs-1.5"
        if padding_name == "pss":
            salt_len = key.size_in_bytes() - hash_image.digest_size - 2
            padding = pss
            padding_args = {'salt_bytes': salt_len}
        elif padding_name == "pkcs-1.5":
            padding = pkcs1_15
            padding_args = {}
        else:
            self.Raise(padding_name + " is not supported")

        sig = padding.new(key, **padding_args).sign(hash_image)

        hash_sig = SHA256.new()
        hash_sig.update(sig)

        version = self.version
        header_size = self.header_size
        image_size = len(self.image)
        ofs_img_sig = 64 + len(sig)
        flags = 0
        reserved0 = 0
        reserved1 = 0

        first_header = struct.pack('>4sIIIIIII32s', PRE_LOAD_MAGIC,
                                   version, header_size, image_size,
                                   ofs_img_sig, flags, reserved0,
                                   reserved1, hash_sig.digest())

        hash_first_header = SHAS[hash_name].new()
        hash_first_header.update(first_header)
        sig_first_header = padding.new(key, **padding_args).sign(hash_first_header)

        data = first_header + sig_first_header + sig
        pad  = bytearray(self.header_size - len(data))

        return data + pad

    def ObtainContents(self):
        """Obtain a placeholder for the header contents"""
        # wait that the image is available
        self.image = self.GetContents(False)
        if self.image is None:
            return False
        self.SetContents(self._CreateHeader())
        return True

    def ProcessContents(self):
        data = self._CreateHeader()
        return self.ProcessContentsUpdate(data)
