using NSec.Cryptography;
namespace DodoSSH.Crypto;
///
/// Ed25519 signatures over the canonical encodings of the specification. See docs/crypto.md §7.
///
///
///
/// 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.
///
///
/// Verification returns 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.
///
///
public static class DshSignatures
{
private static SignatureAlgorithm Algorithm => SignatureAlgorithm.Ed25519;
///
/// Signs a key statement's canonical encoding.
///
/// The signer's Ed25519 key.
/// Output of .
public static byte[] SignKeyStatement(Key signingKey, ReadOnlySpan canonicalStatement)
{
ArgumentNullException.ThrowIfNull(signingKey);
var message = BuildMessage(CryptoSpec.SigningContexts.KeyStatement, canonicalStatement);
return Algorithm.Sign(signingKey, message);
}
///
/// Verifies a key statement's self-signature.
///
///
/// 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.
///
/// Ed25519 public key from the statement itself.
/// Output of .
/// The detached signature.
public static bool VerifyKeyStatement(
ReadOnlySpan signingPublicKey,
ReadOnlySpan canonicalStatement,
ReadOnlySpan 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);
}
///
/// Prefixes a message with its signing context.
///
///
/// Heap-allocated rather than stack-allocated on purpose: the statement contains
/// caller-supplied strings of unbounded length, and a stackalloc sized from untrusted
/// input is a stack overflow waiting to happen.
///
private static byte[] BuildMessage(ReadOnlySpan context, ReadOnlySpan payload)
{
var message = new byte[context.Length + payload.Length];
context.CopyTo(message);
payload.CopyTo(message.AsSpan(context.Length));
return message;
}
}