A digital signature proves that a message matches a private signing key (authenticity) and has not been modified since signing (integrity). Unlike encryption, signing does not hide the content. An ASP.NET Core API commonly verifies RS256 or ES256 tokens with a public key from the issuer’s JWKS endpoint; that result is meaningful only after the issuer and key source are authenticated. HMAC-protected JWTs use a shared-secret MAC instead, so either secret holder can generate them.

How It Works

  1. The signature algorithm encodes the message, normally through an approved hash and algorithm-specific preparation.
  2. The signer applies the private signing operation and emits a signature.
  3. The verifier applies the public verification operation to the received message and signature.
  4. A successful result proves that the message matches the signature under that public key. Trust in the claimed signer still depends on how the public key was authenticated.

“Encrypt the hash with the private key” is not a portable model. RSA-PSS, ECDSA, and EdDSA have different signing mathematics, and none should be implemented by composing raw encryption and hashing operations.

Example in .NET

using System.Security.Cryptography;
using System.Text;
 
// Sign a message
using var rsa = RSA.Create(2048);
var publicKey = rsa.ExportRSAPublicKey();
 
var message = Encoding.UTF8.GetBytes("Transfer $1000 to account 12345");
var signature = rsa.SignData(message, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
 
// Verify the signature
using var verifier = RSA.Create();
verifier.ImportRSAPublicKey(publicKey, out _);
bool isValid = verifier.VerifyData(message, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);

ECDSA P-256 produces smaller signatures than RSA-2048 and is supported by protocols such as JOSE. Use it only when the protocol specifies the curve, hash, and signature encoding and every verifier supports that profile:

using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var message = System.Text.Encoding.UTF8.GetBytes("Transfer $1000 to account 12345");
var signature = ecdsa.SignData(message, HashAlgorithmName.SHA256);
 
// Verify
bool isValid = ecdsa.VerifyData(message, signature, HashAlgorithmName.SHA256);
// ECDSA P-256 signature: 64 bytes vs RSA-2048: 256 bytes

Use Cases

  • JWT signing: The identity provider signs the JWT with its private key; APIs verify with the public key (JWKS endpoint). See JWT Bearer authentication.
  • Code signing: Software publishers sign executables so users can verify the binary has not been tampered with.
  • Document signing: PDF signatures, contract signing (DocuSign uses digital signatures under the hood).
  • TLS certificates: after issuance validation, a CA signature attests the binding between a certificate identity, such as a DNS name, and its public key.

Related Construction: HMAC

HMAC authenticates a message between parties that share one secret. Either party can generate the same MAC, so it does not provide a digital signature’s asymmetric attribution. Request-authentication protocols add canonical request bytes, a timestamp, and a nonce with server-side replay state; those protocol details belong to API authentication. The signature-specific boundary here is trust: a digital signature separates a private signer from public verifiers, while every HMAC verifier is also able to create a valid tag.

Pitfalls

Treating RSA Signing as RSA Encryption

What goes wrong: code applies raw RSA operations to a hand-built hash encoding because signing was described as “encrypting with the private key.” Verification then depends on non-standard parsing and may accept malformed encodings.

Mitigation: call the platform’s signature API with a specified scheme and parameters. Prefer RSA-PSS for a new RSA-based protocol; use PKCS#1 v1.5 signatures only where the protocol requires compatibility. The Bleichenbacher padding oracle concerns PKCS#1 v1.5 encryption, not a reason to describe every PKCS#1 v1.5 signature as decryptable ciphertext.

Trusting Signatures Without Certificate Validation

What goes wrong: verifying a signature with a public key proves the message was signed by whoever holds the corresponding private key — but not that the key belongs to who you think it does. Without certificate validation (chain of trust to a trusted CA), an attacker can substitute their own key pair.

Mitigation: authenticate the key through the trust model the protocol defines, such as a validated X.509 chain or an issuer-bound JWKS document. In JWT, kid is only a selector among keys already obtained for the expected issuer; it does not authenticate the issuer or authorize a new key source. Reject an unknown or ambiguous kid, and never let an untrusted kid, jku, or x5u value redirect verification to an arbitrary key.

Tradeoffs

AlgorithmTypical public-key sizeSignature representationUse when
RSA-PSS2048–4096 bitsModulus-sizedExisting RSA infrastructure or protocol support
ECDSA P-256256 bitsUsually DER-encoded or fixed-width by protocolBroad JOSE, TLS, and platform interoperability
Ed25519256 bits64 bytesProtocols and libraries with explicit Ed25519 support

Choose the algorithm the protocol specifies and the complete client set can verify. Algorithm agility means storing an algorithm or key identifier, supporting a controlled migration, and rejecting unapproved algorithms; it does not mean trusting an unverified message to choose its verifier.

Questions

References