Skip to content

Latest commit

 

History

History
168 lines (129 loc) · 6.67 KB

File metadata and controls

168 lines (129 loc) · 6.67 KB

sml-rsa

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.

Features

  • Key generationgenerate finds two probable primes by Miller-Rabin (via the vendored BigInt), with the random source injected as a randomBytes : int -> string function 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.509 SubjectPublicKeyInfo, and PKCS#8 PrivateKeyInfo, as DER or PEM — built on the vendored sml-asn1 DER codec and sml-pem.

Byte & integer conventions

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.

Algorithms & standards

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.

Key formats

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.

API

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.

Example

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)

Test vectors

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 signaturepkcs1v15sign-vectors.txt, Key B, SHA-1.
  • RSASSA-PSSpss-vect.txt, Key B, SHA-1, 20-byte salt.
  • RSAES-OAEPoaep-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.

Build

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 demo

Requires mlton and/or poly on PATH.

Layout & vendoring

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 (carries INTEGER as BigInt).
  • 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.

Determinism

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.

License

MIT — see LICENSE.