// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// How a certificate's signature is checked: the scheme its algorithm names, with the digest.
priv enum Scheme {
  Pkcs1(@rsa.Digest)
  Pss(@rsa.Digest)
  Ecdsa(@sha2.Kind)
  Eddsa
}

///|
/// The digest an AlgorithmIdentifier inside RSASSA-PSS-params names (RFC 4055 §2.1). Only the
/// SHA-2 digests webpki and Go accept; the parameters must be NULL or absent.
fn pss_digest(t : @asn1.Tlv) -> @rsa.Digest raise @asn1.Refused {
  let (oid, params) = read_alg(t)
  if params is Some(p) && p.to_owned() != b"\x05\x00" {
    raise @asn1.Malformed("digest AlgorithmIdentifier has parameters")
  }
  match oid {
    "2.16.840.1.101.3.4.2.1" => Sha256
    "2.16.840.1.101.3.4.2.2" => Sha384
    "2.16.840.1.101.3.4.2.3" => Sha512
    o => raise @asn1.Unsupported("RSASSA-PSS digest " + o)
  }
}

///|
/// The octet length of a digest.
fn digest_size(d : @rsa.Digest) -> Int32 {
  match d {
    Sha1 => 20
    Sha224 => 28
    Sha256 => 32
    Sha384 => 48
    Sha512 => 64
  }
}

///|
/// RSASSA-PSS-params (RFC 4055 §3.1), held to the one shape anyone issues: SHA-256, SHA-384 or
/// SHA-512, MGF1 over the same digest, a salt as long as the digest, the trailer left at its
/// default. Go and webpki accept exactly this; a certificate signed otherwise is refused as an
/// unsupported algorithm rather than checked under parameters nobody else would honour.
fn pss_params(params : BytesView?) -> @rsa.Digest raise @asn1.Refused {
  let seq = match params {
    Some(p) => @asn1.only(p)
    None =>
      raise @asn1.Unsupported("RSASSA-PSS with default (SHA-1) parameters")
  }
  if seq.tag != 0x30 {
    raise @asn1.Malformed("RSASSA-PSS-params is not a SEQUENCE")
  }
  let mut digest : @rsa.Digest? = None
  let mut mgf : @rsa.Digest? = None
  let mut salt : Int32? = None
  for f in seq.items() {
    let inner = @asn1.only(f.value)
    match f.tag {
      0xa0 => digest = Some(pss_digest(inner))
      0xa1 => {
        let (oid, p) = read_alg(inner)
        if oid != "1.2.840.113549.1.1.8" {
          raise @asn1.Unsupported("RSASSA-PSS mask generation " + oid)
        }
        guard p is Some(p) else {
          raise @asn1.Malformed("MGF1 without a digest")
        }
        mgf = Some(pss_digest(@asn1.only(p)))
      }
      0xa2 => {
        let n = inner.int()
        if n < (0 : BigInt) || n > (1024 : BigInt) {
          raise @asn1.Malformed("RSASSA-PSS salt length out of range")
        }
        salt = Some(n.to_int())
      }
      0xa3 =>
        if inner.int() != (1 : BigInt) {
          raise @asn1.Malformed("RSASSA-PSS trailer field is not 1")
        }
      _ => raise @asn1.Malformed("unexpected field in RSASSA-PSS-params")
    }
  }
  guard digest is Some(d) else {
    raise @asn1.Unsupported("RSASSA-PSS with the default SHA-1 digest")
  }
  if mgf != Some(d) || salt != Some(digest_size(d)) {
    raise @asn1.Unsupported("RSASSA-PSS parameters other than the digest's own")
  }
  d
}

///|
/// The scheme a signature AlgorithmIdentifier names. SHA-1 and SHA-224 signatures are refused:
/// a trust anchor's own signature is never checked, so the only place SHA-1 survives in a
/// modern chain needs no support here.
fn scheme_of(oid : String, params : BytesView?) -> Scheme raise @asn1.Refused {
  let none = params is None
  let null = params is Some(p) && p.to_owned() == b"\x05\x00"
  match oid {
    "1.2.840.113549.1.1.11" if none || null => Pkcs1(Sha256)
    "1.2.840.113549.1.1.12" if none || null => Pkcs1(Sha384)
    "1.2.840.113549.1.1.13" if none || null => Pkcs1(Sha512)
    "1.2.840.113549.1.1.10" => Pss(pss_params(params))
    "1.2.840.10045.4.3.2" if none => Ecdsa(Sha256)
    "1.2.840.10045.4.3.3" if none => Ecdsa(Sha384)
    "1.2.840.10045.4.3.4" if none => Ecdsa(Sha512)
    "1.3.101.112" if none => Eddsa
    "1.2.840.113549.1.1.11"
    | "1.2.840.113549.1.1.12"
    | "1.2.840.113549.1.1.13"
    | "1.2.840.10045.4.3.2"
    | "1.2.840.10045.4.3.3"
    | "1.2.840.10045.4.3.4"
    | "1.3.101.112" =>
      raise @asn1.Malformed(
        "signature algorithm " + oid + " with wrong parameters",
      )
    o => raise @asn1.Unsupported("signature algorithm " + o)
  }
}

///|
/// An ECDSA-Sig-Value (RFC 5480 §2.2) as the fixed-width `r || s` the verifier takes, or `None`
/// when either integer does not fit the curve — such a signature cannot be valid.
fn ecdsa_raw(sig : BytesView, size : Int32) -> Bytes? raise @asn1.Refused {
  let seq = @asn1.only(sig)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("ECDSA-Sig-Value is not a SEQUENCE")
  }
  let rs = seq.items()
  if rs.length() != 2 {
    raise @asn1.Malformed("ECDSA-Sig-Value is not an r and an s")
  }
  let r = rs[0].int()
  let s = rs[1].int()
  if r <= (0 : BigInt) || s <= (0 : BigInt) {
    return None
  }
  let ro = r.to_octets()
  let so = s.to_octets()
  if ro.length() > size || so.length() > size {
    return None
  }
  let buf = Buffer()
  buf.write_bytes(r.to_octets(length=size))
  buf.write_bytes(s.to_octets(length=size))
  Some(buf.to_bytes())
}

///|
/// Check the certificate's signature under `issuer` — its issuer's key, or its own if it is
/// self-signed. RSA PKCS#1 v1.5 and PSS with SHA-256, SHA-384 or SHA-512, ECDSA with the same
/// three digests on any of the four curves, and Ed25519. An algorithm outside that set raises
/// `Unsupported` rather than returning false, so a caller cannot read "not checked" as "checked
/// and bad". A key of the wrong kind for the algorithm returns false: no signature by that
/// algorithm verifies under it.
pub fn Cert::signed_by(self : Cert, issuer : Spki) -> Bool raise @asn1.Refused {
  let scheme = scheme_of(self.alg, self.params.map(p => p[:]))
  if self.sig.unused != 0 {
    raise @asn1.Malformed("signature BIT STRING has unused bits")
  }
  let sig = self.sig.bytes[:]
  match (scheme, issuer.key()) {
    (Pkcs1(d), Rsa(k)) => rsa_with(k, d, Pkcs1).verify(self.tbs, sig)
    (Pss(d), Rsa(k)) =>
      rsa_with(k, d, Pss(salt=digest_size(d))).verify(self.tbs, sig)
    (Ecdsa(kind), Ec(k)) => {
      let curve = k.curve()
      guard ecdsa_raw(sig, curve.size()) is Some(raw) else { return false }
      let key = @ecdsa.PublicKey::with_hash(
        k.bytes()[:],
        () => (@sha2.Hasher::new(kind~) : &@spec.Hash),
        curve~,
      ) catch {
        _ => return false
      }
      key.verify(self.tbs, raw[:])
    }
    (Eddsa, Ed25519(k)) => k.verify(self.tbs, sig)
    _ => false
  }
}

///|
/// `k` re-read with the digest and scheme a signature names.
fn rsa_with(
  k : @rsa.PublicKey,
  digest : @rsa.Digest,
  scheme : @rsa.Scheme,
) -> @rsa.PublicKey raise @asn1.Refused {
  @rsa.PublicKey::new(k.modulus()[:], k.exponent()[:], digest~, scheme~) catch {
    _ => raise @asn1.Malformed("RSAPublicKey carries an unusable modulus")
  }
}