// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The signature schemes a key can sign a CertificateVerify under (RFC 8446 §4.2.3). TLS 1.3 ties
/// an ECDSA scheme to one curve and one hash, so an ECDSA key has exactly one.
fn schemes(key : @x509.PrivateKey) -> Array[Int32] {
match key {
Ec(k) =>
match k.curve() {
P256 => [@ext.ecdsa_secp256r1_sha256]
P384 => [@ext.ecdsa_secp384r1_sha384]
P521 => [@ext.ecdsa_secp521r1_sha512]
Secp256k1 => []
}
Rsa(_) =>
[
@ext.rsa_pss_rsae_sha256, @ext.rsa_pss_rsae_sha384, @ext.rsa_pss_rsae_sha512,
]
Ed25519(_) => [@ext.ed25519]
}
}
///|
/// The schemes a client offers: every one this library can check. Without
/// signature_algorithms_cert the list also speaks for the certificate chain (RFC 8446 §4.2.3),
/// so it ends with the PKCS#1 schemes most chains are signed with, as rustls and OpenSSL send
/// them; a server that finds no chain it may present otherwise closes the connection. A
/// CertificateVerify under PKCS#1 is still refused.
let offered : Array[Int32] = [
@ext.ecdsa_secp256r1_sha256, @ext.ecdsa_secp384r1_sha384, @ext.ecdsa_secp521r1_sha512,
@ext.ed25519, @ext.rsa_pss_rsae_sha256, @ext.rsa_pss_rsae_sha384, @ext.rsa_pss_rsae_sha512,
@ext.rsa_pkcs1_sha256, @ext.rsa_pkcs1_sha384, @ext.rsa_pkcs1_sha512,
]
///|
fn pss_digest(scheme : Int32) -> @rsa.Digest? {
match scheme {
s if s == @ext.rsa_pss_rsae_sha256 => Some(Sha256)
s if s == @ext.rsa_pss_rsae_sha384 => Some(Sha384)
s if s == @ext.rsa_pss_rsae_sha512 => Some(Sha512)
_ => None
}
}
///|
fn pss_salt(d : @rsa.Digest) -> Int32 {
match d {
Sha384 => 48
Sha512 => 64
_ => 32
}
}
///|
/// Sign `content` under `scheme`. An ECDSA signature goes on the wire DER-encoded (RFC 8446
/// §4.2.3), where the library signs the fixed-width `r || s`.
fn sign(
key : @x509.PrivateKey,
scheme : Int32,
content : Bytes,
) -> Bytes raise @alert.Alert {
match key {
Ec(k) => {
let raw = k.sign(content[:])
let half = raw.length() / 2
@asn1.sequence([
@asn1.integer(raw[0:half].to_owned()),
@asn1.integer(raw[half:].to_owned()),
])
}
Ed25519(k) => k.sign(content[:])
Rsa(k) => {
guard pss_digest(scheme) is Some(d) else {
raise @alert.fatal(InternalError)
}
k.with_scheme(digest=d, scheme=Pss(salt=pss_salt(d))).sign(content[:])
}
}
}
///|
/// Whether `sig` over `content` verifies under the leaf certificate's key with `scheme`.
fn verified(
leaf : @x509.Cert,
scheme : Int32,
content : Bytes,
sig : Bytes,
) -> Bool {
let key = leaf.spki.key() catch { _ => return false }
match key {
Ec(k) => {
let curve : @curve.Curve? = match scheme {
s if s == @ext.ecdsa_secp256r1_sha256 => Some(P256)
s if s == @ext.ecdsa_secp384r1_sha384 => Some(P384)
s if s == @ext.ecdsa_secp521r1_sha512 => Some(P521)
_ => None
}
guard curve is Some(c) && c == k.curve() else { return false }
guard ecdsa_raw(sig, c.size()) is Some(raw) else { return false }
k.verify(content[:], raw[:])
}
Ed25519(k) => scheme == @ext.ed25519 && k.verify(content[:], sig[:])
Rsa(k) => {
guard pss_digest(scheme) is Some(d) else { return false }
k
.with_scheme(digest=d, scheme=Pss(salt=pss_salt(d)))
.verify(content[:], sig[:])
}
}
}
///|
/// A DER ECDSA-Sig-Value as the fixed-width `r || s`, or `None` when it is malformed or does not
/// fit the curve.
fn ecdsa_raw(sig : Bytes, size : Int32) -> Bytes? {
let seq = @asn1.only(sig[:]) catch { _ => return None }
let rs = seq.items() catch { _ => return None }
guard seq.tag == 0x30 && rs.length() == 2 else { return None }
let r = rs[0].int() catch { _ => return None }
let s = rs[1].int() catch { _ => return None }
if r <= 0N ||
s <= 0N ||
r.to_octets().length() > size ||
s.to_octets().length() > size {
return None
}
let out = Buffer()
out.write_bytes(r.to_octets(length=size))
out.write_bytes(s.to_octets(length=size))
Some(out.to_bytes())
}