Racket Cryptography Cheat Sheet
Prerequisites
Required Initialization
1(require crypto
2 crypto/all
3 crypto/pem
4 crypto/pkcs8)
5
6; CRITICAL: Must initialize before ANY crypto operations
7(use-all-factories!)
8
9; Without this initialization:
10; Error: "datum->pk-key: unable to read key\n format: 'PrivateKeyInfo"
Rule: Always call (use-all-factories!) at module initialization, not inside functions.
Loading RSA Keys from PEM Files
Reading PEM Files
1; Read PEM file and extract DER-encoded data
2(define (read-pem-file path)
3 (define in (open-input-file path))
4 (define result (read-pem in))
5 (close-input-port in)
6
7 (if (eof-object? result)
8 (error 'read-pem-file "no PEM data found")
9 (values (bytes->string/utf-8 (car result)) ; PEM type (e.g., "PRIVATE KEY")
10 (cdr result)))) ; DER-encoded bytes
Parsing Private Keys with Format Fallback
PEM files can contain PKCS#1 (legacy) or PKCS#8 (modern) format keys:
1; Try PKCS#1 first, fall back to PKCS#8
2(define (parse-private-key der-bytes)
3 (with-handlers
4 ([exn:fail?
5 (λ (e)
6 ; PKCS#1 failed, try PKCS#8
7 (datum->pk-key der-bytes 'PrivateKeyInfo))])
8 ; Try PKCS#1 format first (RSAPrivateKey)
9 (datum->pk-key der-bytes 'RSAPrivateKey)))
Pattern: This dual-format approach handles both OpenSSL legacy format and modern PKCS#8 keys.
Complete Key Loading Function
1(define (load-private-key path)
2 ; Read and parse PEM
3 (define-values (block-type der-bytes)
4 (with-handlers ([exn:fail?
5 (λ (e)
6 (error 'load-private-key
7 "failed to read key: ~a"
8 (exn-message e)))])
9 (read-pem-file path)))
10
11 ; Parse with fallback
12 (define key (parse-private-key der-bytes))
13
14 ; Verify it's RSA
15 (unless (pk-can-encrypt? key)
16 (error 'load-private-key "not an RSA key"))
17
18 key)
19
20; Usage
21(define private-key (load-private-key "certs/private-key.pem"))
Extracting Public Keys
1; Extract public key from loaded private key
2(define public-key (pk-key->public-only-key private-key))
3
4; Verify key properties
5(private-key? private-key) ; => #t
6(public-only-key? public-key) ; => #t
7(pk-can-encrypt? private-key) ; => #t (for RSA)
Pattern: Extract public key immediately after loading private key for later verification operations.
Cryptographic Hash Functions
SHA-256 Digest
1; The digest function accepts both strings and bytes
2(define (sha256-digest data)
3 (digest 'sha256 data))
4
5; Examples
6(sha256-digest "Hello world!") ; Accepts string
7(sha256-digest (string->bytes/utf-8 "Hello world!")) ; Accepts bytes
8; Both produce the same result
Base64-Encoded SHA-256
1(require net/base64)
2
3(define (base64-sha256-digest data)
4 (define hash-bytes (digest 'sha256 data))
5 (bytes->base64-string hash-bytes))
6
7; Usage
8(base64-sha256-digest "Hello world!")
9; => "wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro="
Note: Use net/base64 for RFC 2045 compliance (includes padding =, no newlines).
Digital Signatures (RSA-SHA256)
Complete Signing Workflow
1; Full workflow: string → bytes → digest → sign → base64
2(define (sign-string-base64 message private-key)
3 ; Step 1: Convert string to bytes
4 (define message-bytes (string->bytes/utf-8 message))
5
6 ; Step 2: Create SHA-256 digest
7 (define message-digest (digest 'sha256 message-bytes))
8
9 ; Step 3: Sign digest with PKCS1-v1.5 padding
10 (define signature-bytes
11 (pk-sign-digest private-key
12 'sha256
13 message-digest
14 #:pad 'pkcs1-v1.5))
15
16 ; Step 4: Encode as base64
17 (bytes->base64-string signature-bytes))
Key points:
- Always digest the message first, then sign the digest (not the message directly)
- Use
'pkcs1-v1.5padding for RSA signatures (standard for HTTP Message Signatures) - Result is raw bytes; convert to base64 for HTTP headers
Signature Verification
1(define (verify-signature public-key message signature-bytes)
2 ; Convert message to bytes and digest
3 (define message-bytes (string->bytes/utf-8 message))
4 (define message-digest (digest 'sha256 message-bytes))
5
6 ; Verify signature against digest
7 (pk-verify-digest public-key
8 'sha256
9 message-digest
10 signature-bytes
11 #:pad 'pkcs1-v1.5))
12; Returns #t if valid, #f otherwise
13
14; Usage
15(define message "Hello")
16(define signature (sign-string message private-key))
17(verify-signature public-key message signature) ; => #t
Signing and Verification Contract Pattern
1(provide
2 (contract-out
3 [sign-string-base64 (-> string? string?)]
4 [verify-signature (-> pk-key? string? bytes? boolean?)]))
5
6(define (sign-string-base64 message)
7 (unless (string? message)
8 (error 'sign-string "expected string, got: ~a" message))
9 (bytes->base64-string (sign-string message private-key)))
10
11(define (verify-signature public-key message signature)
12 (define message-bytes (string->bytes/utf-8 message))
13 (define message-digest (digest 'sha256 message-bytes))
14 (pk-verify-digest public-key 'sha256 message-digest signature #:pad 'pkcs1-v1.5))
HTTP Message Signatures
Complete Implementation Pattern
HTTP Message Signatures (RFC draft-cavage-http-signatures) require a multi-step process:
1; Step 1: Create digest of request body
2(define body-digest
3 (string-append "SHA-256=" (base64-sha256-digest request-body)))
4
5; Step 2: Build canonical signature string
6; Format: lowercase header names, specific order, newline-separated
7(define signature-string
8 (format "(request-target): ~a ~a\nhost: ~a\ndate: ~a\ndigest: ~a\ncontent-length: ~a"
9 (string-downcase method) ; "post"
10 path ; "/api/endpoint"
11 host ; "api.example.com"
12 timestamp ; "Thu, 02 Oct 2025 14:30:45 GMT"
13 body-digest ; "SHA-256=..."
14 content-length)) ; "1234"
15
16; Step 3: Sign the canonical string
17(define http-signature (sign-string-base64 signature-string))
18
19; Step 4: Create Authorization header
20(define auth-header
21 (format "Signature keyId=\"~a\", algorithm=\"rsa-sha256\", headers=\"(request-target) host date digest content-length\", signature=\"~a\""
22 key-id
23 http-signature))
Building Signature String from Headers
1(define (make-signature-string #:headers headers
2 #:method method
3 #:path path
4 #:signed-headers signed-headers)
5 ; Helper to get header value (case-insensitive)
6 (define (header-value name)
7 (match (assoc name headers string-ci=?)
8 [(cons _ val) val]
9 [_ (error 'make-signature-string "Missing header: ~a" name)]))
10
11 ; Format headers (lowercase, colon-separated)
12 (define header-lines
13 (for/list ([name signed-headers])
14 (format "~a: ~a"
15 (string-downcase name)
16 (header-value name))))
17
18 ; Build signature string with (request-target) pseudo-header
19 (format "(request-target): ~a ~a\n~a"
20 (string-downcase method)
21 path
22 (string-join header-lines "\n")))
Complete Request Headers Function
1(define (make-http-headers request-path request-host request-body request-time)
2 ; Build standard headers
3 (define headers
4 (list (cons "Host" request-host)
5 (cons "Date" request-time)
6 (cons "Digest" (string-append "SHA-256=" (base64-sha256-digest request-body)))
7 (cons "Content-Length" (number->string (bytes-length (string->bytes/utf-8 request-body))))
8 (cons "Content-Type" "application/xml; charset=UTF-8")))
9
10 ; Create signature string
11 (define sig-string
12 (make-signature-string
13 #:headers headers
14 #:method "POST"
15 #:path request-path
16 #:signed-headers '("host" "date" "digest" "content-length")))
17
18 ; Sign and create Authorization header
19 (define signature (sign-string-base64 sig-string))
20 (define auth-header
21 (cons "Authorization"
22 (format "Signature keyId=\"~a\", algorithm=\"rsa-sha256\", headers=\"(request-target) host date digest content-length\", signature=\"~a\""
23 key-id
24 signature)))
25
26 ; Return all headers including Authorization
27 (append headers (list auth-header)))
Testing Patterns
Key Loading Tests
1(module+ test
2 (require rackunit)
3
4 (test-case "load-private-key loads valid RSA key"
5 (define key (load-private-key "test-key.pem"))
6 (check-true (private-key? key))
7 (check-true (pk-can-encrypt? key)))
8
9 (test-case "load-private-key raises error for non-existent file"
10 (check-exn #rx"failed to read key"
11 (λ () (load-private-key "nonexistent.pem"))))
12
13 (test-case "load-private-key raises error for invalid PEM"
14 (define temp-file (make-temporary-file))
15 (with-output-to-file temp-file #:exists 'truncate
16 (λ () (display "not a PEM file")))
17 (check-exn #rx"no PEM data found"
18 (λ () (load-private-key temp-file)))
19 (delete-file temp-file)))
Digest Tests
1(test-case "sha256-digest accepts both strings and bytes"
2 (define expected #"\300S^K\342\267\237\375\223)\23\5Ck\370\2111NJ?\256\300^\317\374\273}\363\32\331\345\32")
3 (check-equal? (sha256-digest "Hello world!") expected)
4 (check-equal? (sha256-digest (string->bytes/utf-8 "Hello world!")) expected))
5
6(test-case "base64-sha256-digest includes padding and no newlines"
7 (check-equal? (base64-sha256-digest "Hello world!")
8 "wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=")
9
10 ; Long input should not include newlines
11 (define long-text (make-string 500 #\x))
12 (define result (base64-sha256-digest long-text))
13 (check-false (string-contains? result "\n"))
14 (check-false (string-contains? result "\r")))
Signature Tests
1(test-case "sign and verify roundtrip"
2 (define message "Test message")
3 (define signature (sign-string message private-key))
4
5 (check-true (verify-signature public-key message signature))
6 (check-false (verify-signature public-key "Different message" signature))
7 (check-false (verify-signature public-key message (bytes-append signature #"x"))))
8
9(test-case "sign-string-base64 returns valid base64"
10 (define sig (sign-string-base64 "Hello"))
11
12 ; Check it's valid base64 (should decode without error)
13 (check-not-exn (λ () (base64-decode (string->bytes/utf-8 sig))))
14
15 ; Check padding is included
16 (check-true (string-suffix? sig "="))
17
18 ; Check no newlines
19 (check-false (string-contains? sig "\n")))
HTTP Message Signature Tests
1(test-case "make-signature-string formats correctly"
2 (define headers
3 '(("Host" . "example.com")
4 ("Date" . "Thu, 02 Oct 2025 14:30:45 GMT")
5 ("Digest" . "SHA-256=abc123")
6 ("Content-Length" . "42")))
7
8 (define sig-string
9 (make-signature-string
10 #:headers headers
11 #:method "POST"
12 #:path "/api/data"
13 #:signed-headers '("host" "date" "digest" "content-length")))
14
15 (define expected
16 "(request-target): post /api/data\nhost: example.com\ndate: Thu, 02 Oct 2025 14:30:45 GMT\ndigest: SHA-256=abc123\ncontent-length: 42")
17
18 (check-equal? sig-string expected))
Common Patterns
1. String-to-Bytes Conversion Before Crypto
1; ALWAYS convert strings to bytes before cryptographic operations
2(define message-bytes (string->bytes/utf-8 message))
3(define digest (digest 'sha256 message-bytes))
2. Two-Step Digest-Then-Sign
1; DON'T: Sign the message directly
2; (pk-sign private-key message) ; Wrong!
3
4; DO: Digest first, then sign the digest
5(define message-digest (digest 'sha256 (string->bytes/utf-8 message)))
6(define signature (pk-sign-digest private-key 'sha256 message-digest #:pad 'pkcs1-v1.5))
3. Public Key Extraction Pattern
1; Extract public key immediately after loading private key
2(define private-key (load-private-key path))
3(define public-key (pk-key->public-only-key private-key))
4
5; Now you can verify signatures without exposing private key
4. Base64 Encoding for HTTP
1(require net/base64) ; Use net/base64, not base64 package
2
3; Encode bytes to base64 string
4(define encoded (bytes->base64-string crypto-bytes))
5
6; The result includes padding (=) and has no newlines
5. Error Handling with Context
1(define (load-key path)
2 (with-handlers ([exn:fail?
3 (λ (e)
4 (error 'load-key
5 "failed to load key from ~a: ~a"
6 path
7 (exn-message e)))])
8 (load-private-key path)))
Common Gotchas
1. Forgot Crypto Initialization
1; Error: "datum->pk-key: unable to read key"
2; Solution: Add at module level
3(require crypto/all)
4(use-all-factories!)
2. Signing Message Instead of Digest
1; WRONG - signs message bytes directly
2(pk-sign private-key (string->bytes/utf-8 message))
3
4; CORRECT - digest first, then sign
5(pk-sign-digest private-key 'sha256 (digest 'sha256 message-bytes) #:pad 'pkcs1-v1.5)
3. Using Wrong Base64 Library
1; WRONG - different format
2(require base64)
3
4; CORRECT - RFC 2045 MIME standard
5(require net/base64)
4. Forgetting Padding Parameter
1; Missing #:pad parameter may cause verification failures
2(pk-sign-digest private-key 'sha256 digest) ; May use default padding
3
4; Explicit padding ensures compatibility
5(pk-sign-digest private-key 'sha256 digest #:pad 'pkcs1-v1.5)
5. Case-Sensitive Header Comparison
1; WRONG - HTTP headers are case-insensitive
2(assoc "Content-Type" headers)
3
4; CORRECT - use case-insensitive comparison
5(assoc "content-type" headers string-ci=?)
6. Bytes vs String in Signatures
1; sign-string returns bytes
2(define sig-bytes (sign-string message))
3
4; For HTTP headers, convert to base64 string
5(define sig-header (bytes->base64-string sig-bytes))
Security Best Practices
1. Key Storage
1; Use absolute paths for key files
2(define key-path
3 (build-path (find-system-path 'orig-dir) "certs" "private-key.pem"))
4
5; Never hardcode keys in source code
6(define private-key (load-private-key (or (getenv "PRIVATE_KEY_PATH")
7 key-path)))
2. Key Validation
1(define (validate-rsa-key key)
2 (unless (private-key? key)
3 (error 'validate-rsa-key "not a private key"))
4 (unless (pk-can-encrypt? key)
5 (error 'validate-rsa-key "not an RSA key"))
6 key)
3. Signature Verification
1; Always verify signatures before trusting data
2(define (process-signed-message message signature)
3 (unless (verify-signature public-key message signature)
4 (error 'process-signed-message "invalid signature"))
5 (process-message message))
4. Constant-Time Comparison
1; For signature comparison, use built-in verification
2; (it handles timing attacks internally)
3(pk-verify-digest public-key 'sha256 digest signature #:pad 'pkcs1-v1.5)
4
5; DON'T manually compare signature bytes with equal?
Algorithm Reference
Supported Hash Algorithms
1(digest 'sha1 data) ; SHA-1 (legacy, avoid for new code)
2(digest 'sha224 data) ; SHA-224
3(digest 'sha256 data) ; SHA-256 (recommended)
4(digest 'sha384 data) ; SHA-384
5(digest 'sha512 data) ; SHA-512
Supported Padding Schemes
1; For pk-sign-digest and pk-verify-digest
2#:pad 'pkcs1-v1.5 ; RSASSA-PKCS1-v1_5 (most compatible)
3#:pad 'pss ; RSASSA-PSS (more secure, newer)
Recommendation: Use 'pkcs1-v1.5 for maximum compatibility with existing systems (e.g., HTTP Message Signatures). Use 'pss for new systems where you control both ends.
Complete Working Example
1#lang racket
2
3(require crypto
4 crypto/all
5 crypto/pem
6 crypto/pkcs8
7 net/base64)
8
9; Initialize crypto
10(use-all-factories!)
11
12;; Load keys
13(define private-key (load-private-key "private-key.pem"))
14(define public-key (pk-key->public-only-key private-key))
15
16;; Sign a message
17(define message "Important data")
18(define message-bytes (string->bytes/utf-8 message))
19(define message-digest (digest 'sha256 message-bytes))
20(define signature-bytes
21 (pk-sign-digest private-key 'sha256 message-digest #:pad 'pkcs1-v1.5))
22(define signature-b64 (bytes->base64-string signature-bytes))
23
24(displayln (format "Signature: ~a" signature-b64))
25
26;; Verify the signature
27(define valid?
28 (pk-verify-digest public-key
29 'sha256
30 message-digest
31 signature-bytes
32 #:pad 'pkcs1-v1.5))
33
34(displayln (format "Signature valid: ~a" valid?)) ; => #t
Official Documentation and RFCs
- Racket Crypto Library Documentation
- RFC 2045 - MIME Base64
- RFC 8017 - PKCS #1: RSA Cryptography
- HTTP Message Signatures Draft
- PKCS #8 Private Key Format