Summary#
Crypto.HmacSha256Hex proves a message came from someone who holds the same key you do.
That works when you and one partner share a secret. It does not work when you need to prove who you are to a service
you have never shared a secret with — and that is what every machine-to-machine login asks for.
These sign with a private key and verify with the matching public one. The service holds only the public half, so nothing you send it can ever be replayed as your identity.
RS256 and ES256 are the two names you will meet. They are the same idea with different maths: RS256 is RSA and is
what Snowflake, Google and GitHub Apps ask for; ES256 is elliptic-curve, is what Apple asks for, and has much smaller
keys.
Signature#
Crypto.SignRs256(privateKeyPem: string, data: byte[]) → byte[]
Crypto.VerifyRs256(publicKeyPem: string, data: byte[], signature: byte[]) → bool
Crypto.SignEs256(privateKeyPem: string, data: byte[]) → byte[]
Crypto.VerifyEs256(publicKeyPem: string, data: byte[], signature: byte[]) → boolDescription#
The key comes from a secret, never from your source. Declare it, then set it from the file the provider gave you:
app.Secrets = [ new Secret("SnowflakeKey") ];osy secret set SnowflakeKey --file rsa_key.p8--file is not a convenience. A PEM is several lines, and a value typed on the command line reaches your shell
history; the file is how the key arrived and is the right thing to point at.
Both PEM flavours are accepted — -----BEGIN PRIVATE KEY----- (PKCS#8) and -----BEGIN RSA PRIVATE KEY-----
(PKCS#1) — because a key is pasted from whatever the issuer handed over, and being told it is the wrong flavour of the
same key is a refusal with nothing in it. A key that will not parse throws, and the message never quotes the key.
Verify answers false; it does not throw. A signature that does not check out is the ordinary outcome of
examining something you did not write, not an error. Only a malformed key throws, because that one is your own
misconfiguration and reporting it as "the sender is forging" would send you to investigate the wrong end.
These run on the server, always. There is no browser implementation and there will not be one: a private key the browser can observe is a private key that has leaked. See The standard library — and where each call runs for the whole execution-side table.
Examples#
A signed JWT, which is the shape nearly every service wants. The three parts are base64url text joined by dots, and the signature covers the first two:
app.Secrets = [ new Secret("SnowflakeKey"), new Secret("PartnerPublicKey") ];
string B64Url(byte[] b) {
return Convert.ToBase64String(b).Replace("+", "-").Replace("/", "_").Replace("=", "");
}
string MintJwt(string issuer, string subject, long expiresAt) {
var header = B64Url(Text.ToBytes("{\"alg\":\"RS256\",\"typ\":\"JWT\"}"));
var payload = B64Url(Text.ToBytes("{\"iss\":\"" + issuer + "\",\"sub\":\"" + subject + "\",\"exp\":" + expiresAt + "}"));
var signingInput = header + "." + payload;
var signature = Crypto.SignRs256(Secret.SnowflakeKey, Text.ToBytes(signingInput));
return signingInput + "." + B64Url(signature);
}The inbound direction — a webhook signed with a key whose public half the sender published:
[AllowAnonymous]
bool WebhookIsGenuine(string body, string signatureBase64) {
return Crypto.VerifyRs256(
Secret.PartnerPublicKey,
Text.ToBytes(body),
Convert.FromBase64String(signatureBase64));
}See also#
- Crypto.HmacSha256Hex and Crypto.FixedTimeEquals — the shared-key alternative, for when you and one partner hold the same secret
- Signing with raw bytes — Crypto.HmacSha256, Sha256, ToHex, and Text.ToBytes —
Text.ToBytes,Convert.ToBase64Stringand the rest of the byte plumbing these use - reading a secret's value (Secret.Name) — reading a declared secret in a body
- Giving a secret its value —
osy secret set, including--filefor a PEM