Home

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.5 padding 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

Tags: Racket, Cryptography, Security, Digital-Signatures, Rsa, Ssl-Tls