Pure Standard ML RSA: key generation, the PKCS#1 padding schemes (PKCS#1 v1.5 signatures & encryption, OAEP, PSS) and DER/PEM key import/export — anchored on the official RFC 8017 / PKCS#1 v2.1 test vectors, green under both MLton and Poly/ML, with no FFI, no threads, and no randomness that isn't passed in explicitly.
It is the missing half of asymmetric crypto for the sjqtentacles ecosystem and
the signature-verification engine for the upcoming sml-x509.
- Key generation —
generatefinds two probable primes by Miller-Rabin (via the vendoredBigInt), with the random source injected as arandomBytes : int -> stringfunction so generation is deterministic in tests. - RSASSA-PKCS1-v1_5 signatures (
sign/verify) over SHA-1/256/512. - RSAES-PKCS1-v1_5 encryption (
encrypt/decrypt), random padding injected. - RSAES-OAEP encryption (
encryptOaep/decryptOaep) with MGF1 and an optional label; the seed is injected. - RSASSA-PSS signatures (
signPss/verifyPss) with MGF1; the salt is injected. - Key import/export in PKCS#1 (
RSAPublicKey/RSAPrivateKey), X.509SubjectPublicKeyInfo, and PKCS#8PrivateKeyInfo, as DER or PEM — built on the vendoredsml-asn1DER codec andsml-pem.
Every cryptographic value — messages, signatures, ciphertexts, seeds, salts,
DER blobs — is a raw string: one byte per char, codepoints 0..255.
Arbitrary-precision integers (moduli, exponents, primes) are the vendored
BigInt.int. toHex / fromHex convert raw bytes to and from lowercase
hexadecimal.
| Scheme | Function | Reference |
|---|---|---|
| RSAEP / RSAVP1, RSADP / RSASP1 (CRT) | internal primitives | RFC 8017 §5 |
| EMSA-PKCS1-v1_5 signature | sign / verify |
RFC 8017 §8.2, §9.2 |
| EME-PKCS1-v1_5 encryption | encrypt / decrypt |
RFC 8017 §7.2 |
| EME-OAEP (MGF1) | encryptOaep / decryptOaep |
RFC 8017 §7.1 |
| EMSA-PSS (MGF1) | signPss / verifyPss |
RFC 8017 §8.1, §9.1 |
DigestInfo (SHA-1/256/512) |
internal | RFC 8017 §9.2 |
Hashes (SHA1, SHA256, SHA512) come from the vendored sml-codec.
| Form | Encoder / decoder | PEM label |
|---|---|---|
PKCS#1 RSAPublicKey |
encodePublicDer / decodePublicDer |
RSA PUBLIC KEY |
PKCS#1 RSAPrivateKey |
encodePrivateDer / decodePrivateDer |
RSA PRIVATE KEY |
X.509 SubjectPublicKeyInfo |
encodeSpkiDer / decodeSpkiDer |
PUBLIC KEY |
PKCS#8 PrivateKeyInfo |
encodePkcs8Der / decodePkcs8Der |
PRIVATE KEY |
encodePublicPem / encodePrivatePem emit the generic SPKI / PKCS#8 forms;
decodePublicPem / decodePrivatePem accept either the generic wrapping or the
bare PKCS#1 form, so real OpenSSL-generated keys import unchanged.
type pubkey = { n : BigInt.int, e : BigInt.int }
type privkey = { n : BigInt.int, e : BigInt.int, d : BigInt.int
, p : BigInt.int, q : BigInt.int
, dp : BigInt.int, dq : BigInt.int, qinv : BigInt.int }
datatype hash = SHA1 | SHA256 | SHA512
val generate : { bits:int, e:BigInt.int, randomBytes:int -> string } -> keypair
(* the entry points sml-x509 uses to check a certificate signature *)
val sign : { priv:privkey, hash:hash, msg:string } -> string
val verify : { pub:pubkey, hash:hash, msg:string, sgn:string } -> bool
val decodePublicPem : string -> pubkey (* accepts SPKI or PKCS#1 *)
val decodePrivatePem : string -> privkey (* accepts PKCS#8 or PKCS#1 *)See src/rsa.sig for the full signature.
val { pub, priv } =
Rsa.generate { bits = 1024, e = BigInt.fromInt 65537, randomBytes = myRng }
val sgn = Rsa.sign { priv = priv, hash = Rsa.SHA256, msg = "hello" }
val ok = Rsa.verify { pub = pub, hash = Rsa.SHA256, msg = "hello", sgn = sgn }make example runs examples/demo.sml, which generates a
key from a fixed seed, exports it as PEM, and signs/verifies a message with both
PKCS#1 v1.5 and PSS:
sml-rsa demo
============
Generated a fresh 1024-bit key (deterministic seed). Public key:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5aOvXw5N+jKHeh/BEVynsuitP
VoKJXII5m/rPvaJTIdwioLPkE2xAVWb6KTkz1bO4RP1nwq57LN2kPYVSVwf2oykv
9SqJYwNfExowLSWrfgCtsc0fQn+/Kk7Cjdzy9U+ZJO738QxSwoCYvWvnUOkNhYgA
0rJ2Z3KrlMgBmin64QIDAQAB
-----END PUBLIC KEY-----
message : The quick brown fox jumps over the lazy dog
PKCS#1 v1.5 signature: 9080260472d9b1727b57e421150f735c...995466
verify : true
PSS signature : 8f6b2a421dc0ca558f5569f7368441678c...ce3786
verify : true
tampered signature : false (expected false)
The test suite checks the three padding schemes byte-for-byte against the official PKCS#1 v2.1 vector files referenced by RFC 8017 (Example 1.1 of each):
- PKCS#1 v1.5 signature —
pkcs1v15sign-vectors.txt, Key B, SHA-1. - RSASSA-PSS —
pss-vect.txt, Key B, SHA-1, 20-byte salt. - RSAES-OAEP —
oaep-vect.txt, Key A, SHA-1, empty label.
It also imports a real OpenSSL-generated 1024-bit key in PKCS#8, PKCS#1 and SPKI PEM form, performs deterministic key generation with a sign/verify and encrypt/decrypt round-trip, and rejects tampered signatures and ciphertexts — 30 checks, identical under MLton and Poly/ML.
make test # build + run the suite under MLton
make test-poly # run the suite under Poly/ML
make all-tests # both
make example # build + run the demoRequires mlton and/or poly on PATH.
Layout B: the library lives in src/; its dependencies are vendored verbatim
(byte-identical to their origin/main, verified with diff -rq) under
lib/github.com/sjqtentacles/:
sml-bigint— arbitrary-precision integers (modpow, Miller-Rabin).sml-codec— SHA-1/256/512 and Base64.sml-asn1— DER codec (carriesINTEGERasBigInt).sml-pem— PEM framing.
There are two dependency diamonds — sml-rsa needs BigInt (also used by
sml-asn1) and the SHA codec (also used by sml-pem). Each shared dependency
is pulled in along a single path (sml-asn1 brings BigInt, sml-pem
brings the codec; see src/rsa.mlb), so no structure is defined
twice. The Poly/ML use-chain in the Makefile mirrors that order.
No FFI, threads, wall-clock or OS randomness. All randomness is an explicit
argument (randomBytes, seed, salt), so every operation is reproducible
and byte-identical across MLton and Poly/ML — which is exactly what makes the
official RFC 8017 vectors checkable to the byte.
MIT — see LICENSE.