Application Security scripts
Application Security
The application security scripts and packages I've written, with their source shown directly below โ no need to open GitHub to read them.
OAuth PKCE (S256) verifier and generator
GoRead-onlyGo test suite
pkce.go Go ยท 85 lines
appsec/scripts/oauth-pkce/pkce.go
1// Package oauthpkce is a bounded educational S256-only OAuth PKCE verifier.
2// It deliberately never logs verifiers or challenges.
3package oauthpkce
4
5import (
6 "crypto/rand"
7 "crypto/sha256"
8 "crypto/subtle"
9 "encoding/base64"
10 "errors"
11 "fmt"
12 "math/big"
13 "regexp"
14)
15
16const (
17 MinVerifierLength = 43
18 MaxVerifierLength = 128
19)
20
21var verifierPattern = regexp.MustCompile(`^[A-Za-z0-9\-._~]+$`)
22
23// IsValidCodeVerifier enforces the RFC 7636 length and unreserved-character rules.
24func IsValidCodeVerifier(verifier string) bool {
25 length := len(verifier)
26 return length >= MinVerifierLength &&
27 length <= MaxVerifierLength &&
28 verifierPattern.MatchString(verifier)
29}
30
31// GenerateRandomVerifier returns an RFC 7636 verifier generated from crypto/rand.
32func GenerateRandomVerifier(length int) (string, error) {
33 if length < MinVerifierLength || length > MaxVerifierLength {
34 return "", fmt.Errorf(
35 "verifier length must be between %d and %d characters",
36 MinVerifierLength,
37 MaxVerifierLength,
38 )
39 }
40
41 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
42 alphabetLength := big.NewInt(int64(len(alphabet)))
43 verifier := make([]byte, length)
44 for index := range verifier {
45 randomIndex, err := rand.Int(rand.Reader, alphabetLength)
46 if err != nil {
47 return "", fmt.Errorf("generate verifier entropy: %w", err)
48 }
49 verifier[index] = alphabet[randomIndex.Int64()]
50 }
51 return string(verifier), nil
52}
53
54// ComputeChallengeS256 derives BASE64URL(SHA-256(ASCII(code_verifier))).
55func ComputeChallengeS256(verifier string) (string, error) {
56 if !IsValidCodeVerifier(verifier) {
57 return "", errors.New("code_verifier does not satisfy RFC 7636 syntax")
58 }
59 sum := sha256.Sum256([]byte(verifier))
60 return base64.RawURLEncoding.EncodeToString(sum[:]), nil
61}
62
63func isValidS256Challenge(challenge string) bool {
64 decoded, err := base64.RawURLEncoding.Strict().DecodeString(challenge)
65 return err == nil && len(decoded) == sha256.Size
66}
67
68// ValidatePKCES256 validates one token request against its stored authorization
69// transaction. It rejects method downgrade and malformed inputs before performing a
70// constant-time comparison of equal-length S256 challenges.
71func ValidatePKCES256(codeVerifier, storedChallenge, method string) bool {
72 if method != "S256" || !IsValidCodeVerifier(codeVerifier) ||
73 !isValidS256Challenge(storedChallenge) {
74 return false
75 }
76
77 computedChallenge, err := ComputeChallengeS256(codeVerifier)
78 if err != nil {
79 return false
80 }
81 return subtle.ConstantTimeCompare(
82 []byte(computedChallenge),
83 []byte(storedChallenge),
84 ) == 1
85}
1package oauthpkce_test
2
3import (
4 "testing"
5
6 oauthpkce "github.com/jasonachkar/cybersecurity-writeups/appsec/scripts/oauth-pkce"
7)
8
9func TestValidatePKCES256(t *testing.T) {
10 const vectorVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
11 const vectorChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
12 const wrongVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXa"
13 const invalidCharacterVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjX!"
14
15 tests := []struct {
16 name string
17 verifier string
18 challenge string
19 method string
20 want bool
21 }{
22 {"RFC 7636 S256 vector is accepted", vectorVerifier, vectorChallenge, "S256", true},
23 {"wrong verifier is rejected", wrongVerifier, vectorChallenge, "S256", false},
24 {"plain method downgrade is rejected", vectorVerifier, vectorVerifier, "plain", false},
25 {"method names are not normalized", vectorVerifier, vectorChallenge, "s256", false},
26 {"short verifier is rejected", "too-short", vectorChallenge, "S256", false},
27 {"non-unreserved character is rejected", invalidCharacterVerifier, vectorChallenge, "S256", false},
28 {"malformed stored challenge is rejected", vectorVerifier, "not-base64url!", "S256", false},
29 }
30
31 for _, tt := range tests {
32 t.Run(tt.name, func(t *testing.T) {
33 got := oauthpkce.ValidatePKCES256(tt.verifier, tt.challenge, tt.method)
34 if got != tt.want {
35 t.Fatalf("ValidatePKCES256() = %v, want %v", got, tt.want)
36 }
37 })
38 }
39}
40
41func TestGeneratedVerifierRoundTrip(t *testing.T) {
42 verifier, err := oauthpkce.GenerateRandomVerifier(64)
43 if err != nil {
44 t.Fatalf("GenerateRandomVerifier: %v", err)
45 }
46 if !oauthpkce.IsValidCodeVerifier(verifier) {
47 t.Fatal("generated verifier failed syntax validation")
48 }
49 challenge, err := oauthpkce.ComputeChallengeS256(verifier)
50 if err != nil {
51 t.Fatalf("ComputeChallengeS256: %v", err)
52 }
53 if !oauthpkce.ValidatePKCES256(verifier, challenge, "S256") {
54 t.Fatal("generated verifier round trip was rejected")
55 }
56}
What it does
Implements RFC 7636 S256 verifier generation, challenge derivation, and constant-time verification.
Why it exists
The OAuth/OIDC research needed a runnable PKCE reference implementation behind its boundary tests.
Permissions and safety
None. It performs no filesystem, network, or credential access.
Usage
go test ./appsec/scripts/oauth-pkce/...
What was tested
The suite covers successful round trips, wrong verifiers, downgrade attempts, invalid lengths and characters, and malformed challenges.
Limitations
- It covers PKCE mechanics only, not the complete OAuth authorization flow.
- It is a package rather than a standalone CLI.