// The detached signature of a package.
//
// Everything a package says about itself can be rewritten by whoever holds
// the file: the manifest is in the file, and the digest that goes with it is
// not a secret. So a package cannot tell the host that it is the package the
// host published.
//
// The signature is the material that can. The host holds the secret key,
// signs the digest of the package and publishes the signature next to it.
// Whoever holds the public key can then check that the package is the one
// the host published, without trusting anything the package says about
// itself.
//
// The signature covers the digest of the package and not the package itself,
// so signing a package does not need the package to be held in memory and
// the signature stays the same size whatever the package holds.
//
// The signature is an Ed25519 one, so the public key is enough to check it
// and the secret key is enough to make it.

///|
/// The label that the signature of a package is computed over.
///
/// The label keeps a signature of a package from being a signature of
/// anything else that happens to have the same digest.
const LABEL_PACKAGE_SIGNATURE : String = "lunapack-v6|package-signature"

///|
/// The first line of a signature file.
pub const SIGNATURE_HEADER : String = "lunapack-signature v1"

///|
/// The algorithm of a signature file.
pub const SIGNATURE_ALGORITHM : String = "ed25519"

///|
/// A detached signature of a package.
pub struct PackageSignature {
  /// The SHA-256 of the package, hex.
  package_digest : String
  /// The SHA-256 of the manifest of the package, hex.
  manifest_digest : String
  /// The signature itself, hex.
  signature : String
}

///|
/// Constructor so callers in other packages can build a PackageSignature.
pub fn new_package_signature(
  package_digest : String,
  manifest_digest : String,
  signature : String,
) -> PackageSignature {
  { package_digest, manifest_digest, signature, }
}

///|
/// The bytes that the signature of a package is computed over.
fn signature_message(package_digest : Bytes) -> Bytes {
  bytes_concat(string_to_utf8_bytes(LABEL_PACKAGE_SIGNATURE), package_digest)
}

///|
/// Sign the package at `path` with the secret key `secret`.
///
/// The secret key is the one of the pair: the seed of the signing key is
/// derived from it, so the key that signs is not the key that decrypts.
///
/// The result says which package it belongs to: the digest of the package
/// and the digest of its manifest.
pub async fn sign_package(path : String, secret : Bytes) -> PackageSignature {
  let data = @fs.read_binary(path)
  let digest = sha256_hex(data)
  let digest_bytes = match hex_to_bytes(digest) {
    Some(b) => b
    None => fail("the digest of the package could not be read")
  }
  let manifest_digest = package_manifest_digest(path)
  let signature = ed25519_sign(
    ed25519_seed(secret),
    signature_message(digest_bytes),
  )
  new_package_signature(digest, manifest_digest, bytes_to_hex(signature))
}

///|
/// The text of a signature file.
///
/// The file is a text, so that a person can read what it says and a script
/// can check it without a library.
pub fn signature_to_text(sig : PackageSignature) -> String {
  let mut out = SIGNATURE_HEADER + "\n"
  out = out + "algorithm " + SIGNATURE_ALGORITHM + "\n"
  out = out + "package " + sig.package_digest + "\n"
  out = out + "manifest " + sig.manifest_digest + "\n"
  out = out + "signature " + sig.signature + "\n"
  out
}

///|
/// Are the two texts the same hexadecimal digest?
///
/// The digest of a package is written in small letters, and the one that is
/// given may be written in big ones, so the letters of the hexadecimal
/// alphabet are compared without regard to their case.
fn same_hex(a : String, b : String) -> Bool {
  if a.length() != b.length() {
    return false
  }
  let mut i = 0
  while i < a.length() {
    let x = a[i].to_int()
    let y = b[i].to_int()
    let small_x = if x >= 65 && x <= 70 { x + 32 } else { x }
    let small_y = if y >= 65 && y <= 70 { y + 32 } else { y }
    if small_x != small_y {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// The signature in the text of a signature file.
///
/// Returns None when the text is not a signature file or when a field of it
/// is not what it has to be.
pub fn signature_from_text(text : String) -> PackageSignature? {
  let lines = text_lines(text)
  if lines.length() < 5 {
    return None
  }
  if lines[0] != SIGNATURE_HEADER {
    return None
  }
  if lines[1] != "algorithm " + SIGNATURE_ALGORITHM {
    return None
  }
  let package_digest = lines[2]
  let manifest_digest = lines[3]
  let signature = lines[4]
  if !package_digest.has_prefix("package ") ||
    !manifest_digest.has_prefix("manifest ") ||
    !signature.has_prefix("signature ") {
    return None
  }
  Some(
    new_package_signature(
      package_digest["package ".length():].to_owned(),
      manifest_digest["manifest ".length():].to_owned(),
      signature["signature ".length():].to_owned(),
    ),
  )
}

///|
/// Does the signature hold for the package at `path`?
///
/// `public` is the signing public key of the host: the Ed25519 one, not the
/// one a package is sealed to.
///
/// The digest of the package is computed again and compared with the digest
/// the signature was made over, and the signature itself is checked with
/// the public key. A package that was changed after it was signed does not
/// match the digest, and a signature that was made over another package
/// does not hold.
pub async fn verify_package_signature(
  path : String,
  sig : PackageSignature,
  public : Bytes,
) -> Bool {
  let data = @fs.read_binary(path)
  let digest = sha256_hex(data)
  if digest != sig.package_digest {
    println(
      "verify: the package does not have the digest the signature was made over",
    )
    return false
  }
  let digest_bytes = match hex_to_bytes(digest) {
    Some(b) => b
    None => return false
  }
  let signature = match hex_to_bytes(sig.signature) {
    Some(b) => b
    None => {
      println("verify: the signature is not hexadecimal")
      return false
    }
  }
  if !ed25519_verify(public, signature_message(digest_bytes), signature) {
    println("verify: the signature does not hold for the public key")
    return false
  }
  true
}

///|
/// Does the package at `path` have the digest `expected`?
///
/// The digest is the anchor that a host publishes next to a package. It
/// needs no key at all, so it works for a package that anybody may hold.
pub async fn verify_package_digest(path : String, expected : String) -> Bool {
  let data = @fs.read_binary(path)
  let digest = sha256_hex(data)
  if !same_hex(digest, expected.trim().to_owned()) {
    println(
      "verify: the package has the digest " +
      digest +
      ", but " +
      expected +
      " was expected",
    )
    return false
  }
  true
}