using NSec.Cryptography;

namespace DodoSSH.Crypto;

/// <summary>
/// Ed25519 signatures over the canonical encodings of the specification. See docs/crypto.md §7.
/// </summary>
/// <remarks>
/// <para>
/// Every signature is prefixed with a context string, so a signature produced in one role can
/// never be replayed in another. Without that, a key statement signature and a grant signature
/// over coincidentally-equal bytes would be interchangeable.
/// </para>
/// <para>
/// Verification returns <see langword="false"/> rather than throwing, including for malformed
/// keys and signatures. These values arrive from an untrusted server or peer, so rejection is an
/// expected outcome to be handled at the call site, not an exceptional one.
/// </para>
/// </remarks>
public static class DshSignatures
{
    private static SignatureAlgorithm Algorithm => SignatureAlgorithm.Ed25519;

    /// <summary>
    /// Signs a key statement's canonical encoding.
    /// </summary>
    /// <param name="signingKey">The signer's Ed25519 key.</param>
    /// <param name="canonicalStatement">Output of <see cref="KeyStatementCodec.Encode"/>.</param>
    public static byte[] SignKeyStatement(Key signingKey, ReadOnlySpan<byte> canonicalStatement)
    {
        ArgumentNullException.ThrowIfNull(signingKey);

        var message = BuildMessage(CryptoSpec.SigningContexts.KeyStatement, canonicalStatement);
        return Algorithm.Sign(signingKey, message);
    }

    /// <summary>
    /// Verifies a key statement's self-signature.
    /// </summary>
    /// <remarks>
    /// The statement carries the very public key that verifies it, so this proves only that
    /// whoever produced the statement held the matching private key. It says nothing about who
    /// that is — that is the identity provider binding's job. Both checks are required; neither
    /// substitutes for the other.
    /// </remarks>
    /// <param name="signingPublicKey">Ed25519 public key from the statement itself.</param>
    /// <param name="canonicalStatement">Output of <see cref="KeyStatementCodec.Encode"/>.</param>
    /// <param name="signature">The detached signature.</param>
    public static bool VerifyKeyStatement(
        ReadOnlySpan<byte> signingPublicKey,
        ReadOnlySpan<byte> canonicalStatement,
        ReadOnlySpan<byte> signature)
    {
        if (signingPublicKey.Length != CryptoSpec.PublicKeySize
            || signature.Length != CryptoSpec.SignatureSize)
        {
            return false;
        }

        PublicKey publicKey;
        try
        {
            publicKey = PublicKey.Import(Algorithm, signingPublicKey, KeyBlobFormat.RawPublicKey);
        }
        catch (FormatException)
        {
            return false;
        }

        var message = BuildMessage(CryptoSpec.SigningContexts.KeyStatement, canonicalStatement);
        return Algorithm.Verify(publicKey, message, signature);
    }

    /// <summary>
    /// Prefixes a message with its signing context.
    /// </summary>
    /// <remarks>
    /// Heap-allocated rather than stack-allocated on purpose: the statement contains
    /// caller-supplied strings of unbounded length, and a <c>stackalloc</c> sized from untrusted
    /// input is a stack overflow waiting to happen.
    /// </remarks>
    private static byte[] BuildMessage(ReadOnlySpan<byte> context, ReadOnlySpan<byte> payload)
    {
        var message = new byte[context.Length + payload.Length];
        context.CopyTo(message);
        payload.CopyTo(message.AsSpan(context.Length));

        return message;
    }
}
