How Passkeys Rely on Private & Public Key Signing
This article explains simply how private and public key cryptography is used to sign messages and later verify those signatures. The goal is to build correct intuition for engineers and technical readers who want to understand what actually happens when systems like passkeys rely on cryptographic signing.
The core problem
When a system receives a message, it often needs to answer a very specific question:
Did this exact message come from someone who owns a particular private key?
This is not about secrecy. The message itself can be public. What matters is authenticity and integrity.
The key pair
Public‑key cryptography always starts with a key pair:
-
Private key
- Secret
- Stored securely
- Never shared
-
Public key
- Shared freely
- Stored by servers or other parties
The keys are mathematically related, but in a one‑way fashion: knowing the public key does not allow you to derive the private key.
What “signing a message” really means
Signing does not mean encrypting the original message.
Instead, signing means:
- Creating a fingerprint of the message
- Using the private key to create a proof tied to that fingerprint
The output is called a signature.
The signature proves two things at once:
- The message was not altered
- The signer owns the private key
Step 1 — Hash the message
Messages can be any size, but cryptographic algorithms work with fixed‑size inputs. To solve this, the message is passed through a hash function.
A hash function:
- Produces a fixed‑size output
- Always gives the same result for the same input
- Changes completely if the input changes even slightly
Example:
Message: "Login challenge: 938472"
Hash: 8F3A91...
This hash is the message’s digital fingerprint.
Step 2 — Sign the hash with the private key
The private key is then used to transform the hash into a signature.
Conceptually:
signature = SIGN(hash, private_key)
Important properties:
- Only the private key can produce this signature
- The signature is tied to the exact hash
- Changing the message breaks the signature
Modern schemes add randomness and formatting at this step to prevent attacks, but the idea remains the same.
Step 3 — Send message and signature
The signer sends:
- The original message (or a known challenge)
- The signature
The private key is never transmitted.
Step 4 — Verify with the public key
Verification is the mirror operation performed by the receiver.
The verifier:
- Hashes the received message
- Uses the public key to check the signature
- Confirms that both results match
Conceptually:
expected_hash = HASH(message)
verified_hash = VERIFY(signature, public_key)
if expected_hash == verified_hash:
signature is valid
The public key does not reveal the private key. It can only answer one question:
Is this signature mathematically consistent with this message and this public key?
Why this works
The mathematics behind public‑key cryptography is intentionally asymmetric:
- Creating a signature requires secret information
- Verifying a signature is easy and public
This asymmetry makes forgery infeasible while keeping verification cheap and scalable.
What signing is not
It is important to avoid common misconceptions:
- Signing does not hide the message
- Signing does not send the private key
- Signing does not rely on shared secrets
If secrecy is required, encryption is used in addition to signing, not instead of it.
Why modern systems use signing
Cryptographic signing is a core building block behind passkeys, where devices prove ownership of a private key without sharing secrets.
This approach allows systems to rely on mathematical proof, rather than passwords or human memory.
Final takeaway
Signing a message means:
Turning a message into a fingerprint and using a private key to produce a proof that anyone can later verify using the matching public key.
This simple idea underpins most modern secure systems.
Research Notes
Expand to read the full research notes, references, and comparisons gathered for this topic.
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
# Generate keys
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
# Generate a second pair for the failing scenario
private_key_2 = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
message = b"Login challenge: 938472"
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
# The verification is successful for the first scenario
from cryptography.exceptions import InvalidSignature
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
print("Signature is VALID")
except InvalidSignature:
print("Signature is INVALID")
# Naturally, the verification fails when the signature is generated with a different private key
# than the expected by the stored public key in the server
signature_2 = private_key_2.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
try:
public_key.verify(
signature_2,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
print("Signature is VALID")
except InvalidSignature:
print("Signature is INVALID")