-
Notifications
You must be signed in to change notification settings - Fork 1
Add post-quantum cryptography support with ML-DSA-65 #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
therealpaulgg
wants to merge
9
commits into
main
Choose a base branch
from
claude/quantum-resistant-key-generation-XTHBx
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
6e371a5
Add quantum-resistant ML-DSA-65 JWT verification and key migration su…
claude c4e4131
debugging updates
therealpaulgg 6936a16
start using shared library for my sanity
therealpaulgg a3cccd7
remove go work
therealpaulgg 83d5435
Pivot to hybrid ECDH P-256 + ML-KEM-768 key exchange scheme
claude 98355f2
Revert "Pivot to hybrid ECDH P-256 + ML-KEM-768 key exchange scheme"
therealpaulgg e51e88f
no hybrid crypto, MLDSA65 -> MLDSA
therealpaulgg a3d5ebc
PR comments
therealpaulgg 2ff252e
deslop
therealpaulgg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| FROM golang:1.26-alpine AS builder | ||
therealpaulgg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ENV GOOS=linux | ||
| ENV GOARCH=amd64 | ||
| ENV CGO_ENABLED=0 | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY . . | ||
| COPY ./ssh-sync .ssh-sync | ||
|
|
||
| RUN go mod download | ||
| RUN go mod verify | ||
|
|
||
| RUN go test ./... -cover | ||
| RUN go build -o /godocker | ||
|
|
||
| FROM scratch | ||
|
|
||
| WORKDIR / | ||
|
|
||
| COPY --from=builder /godocker /godocker | ||
|
|
||
| ENV NO_DOTENV=1 | ||
| ENTRYPOINT ["/godocker"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| package crypto | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "encoding/pem" | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "filippo.io/mldsa" | ||
| "github.com/lestrrat-go/jwx/v2/jwa" | ||
| "github.com/lestrrat-go/jwx/v2/jwk" | ||
| ) | ||
therealpaulgg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // KeyType represents the cryptographic algorithm family of a public key. | ||
| type KeyType int | ||
|
|
||
| const ( | ||
| KeyTypeUnknown KeyType = iota | ||
| KeyTypeECDSA | ||
| KeyTypeMLDSA | ||
| ) | ||
|
|
||
| // DetectKeyType inspects the PEM block type to determine the key algorithm. | ||
| func DetectKeyType(pemBytes []byte) KeyType { | ||
| block, _ := pem.Decode(pemBytes) | ||
| if block == nil { | ||
| return KeyTypeUnknown | ||
| } | ||
| switch block.Type { | ||
| case "PUBLIC KEY": | ||
| return KeyTypeECDSA | ||
| case "MLDSA PUBLIC KEY": | ||
| return KeyTypeMLDSA | ||
therealpaulgg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| default: | ||
| return KeyTypeUnknown | ||
| } | ||
| } | ||
|
|
||
| // ParseMLDSAPublicKey extracts an ML-DSA public key from PEM-encoded bytes. | ||
| func ParseMLDSAPublicKey(pemBytes []byte) (*mldsa.PublicKey, error) { | ||
| block, _ := pem.Decode(pemBytes) | ||
| if block == nil { | ||
| return nil, errors.New("failed to decode PEM block") | ||
| } | ||
| if block.Type != "MLDSA PUBLIC KEY" { | ||
| return nil, fmt.Errorf("unexpected PEM block type: %s", block.Type) | ||
| } | ||
| pk, err := mldsa.NewPublicKey(mldsa.MLDSA65(), block.Bytes) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to parse ML-DSA public key: %w", err) | ||
| } | ||
| return pk, nil | ||
| } | ||
|
|
||
| // ValidatePublicKey validates that the given PEM bytes contain a supported public key. | ||
| // Returns the detected KeyType on success. | ||
| func ValidatePublicKey(pemBytes []byte) (KeyType, error) { | ||
| kt := DetectKeyType(pemBytes) | ||
| switch kt { | ||
| case KeyTypeECDSA: | ||
| key, err := jwk.ParseKey(pemBytes, jwk.WithPEM(true)) | ||
| if err != nil { | ||
| return KeyTypeUnknown, fmt.Errorf("invalid ECDSA key: %w", err) | ||
| } | ||
| if key.KeyType() != jwa.EC { | ||
| return KeyTypeUnknown, errors.New("key is not EC type") | ||
| } | ||
| return KeyTypeECDSA, nil | ||
| case KeyTypeMLDSA: | ||
| if _, err := ParseMLDSAPublicKey(pemBytes); err != nil { | ||
| return KeyTypeUnknown, err | ||
| } | ||
| return KeyTypeMLDSA, nil | ||
| default: | ||
| return KeyTypeUnknown, errors.New("unsupported key type") | ||
| } | ||
| } | ||
|
|
||
| type jwtHeader struct { | ||
| Alg string `json:"alg"` | ||
| Typ string `json:"typ"` | ||
| } | ||
|
|
||
| // DetectJWTAlgorithm reads the JWT header to determine the signing algorithm. | ||
| func DetectJWTAlgorithm(tokenString string) (string, error) { | ||
| parts := strings.SplitN(tokenString, ".", 3) | ||
| if len(parts) != 3 { | ||
| return "", errors.New("invalid JWT format") | ||
| } | ||
| headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to decode JWT header: %w", err) | ||
| } | ||
| var header jwtHeader | ||
| if err := json.Unmarshal(headerBytes, &header); err != nil { | ||
| return "", fmt.Errorf("failed to parse JWT header: %w", err) | ||
| } | ||
| return header.Alg, nil | ||
| } | ||
|
|
||
| type jwtClaims struct { | ||
| Username string `json:"username"` | ||
| Machine string `json:"machine"` | ||
| Exp float64 `json:"exp"` | ||
| } | ||
|
|
||
| // ExtractJWTClaims manually extracts username and machine claims from a JWT | ||
| // without verification. This is used as a fallback when lestrrat-go/jwx | ||
| // cannot parse the token (e.g., unrecognized algorithm like MLDSA65). | ||
therealpaulgg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func ExtractJWTClaims(tokenString string) (username, machine string, err error) { | ||
| parts := strings.SplitN(tokenString, ".", 3) | ||
| if len(parts) != 3 { | ||
| return "", "", errors.New("invalid JWT format") | ||
| } | ||
| payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) | ||
| if err != nil { | ||
| return "", "", fmt.Errorf("failed to decode JWT payload: %w", err) | ||
| } | ||
| var claims jwtClaims | ||
| if err := json.Unmarshal(payloadBytes, &claims); err != nil { | ||
| return "", "", fmt.Errorf("failed to parse JWT claims: %w", err) | ||
| } | ||
| return claims.Username, claims.Machine, nil | ||
| } | ||
|
|
||
| // VerifyMLDSAJWT verifies a JWT signed with ML-DSA and checks expiration. | ||
| func VerifyMLDSAJWT(tokenString string, pubKey *mldsa.PublicKey) error { | ||
| parts := strings.SplitN(tokenString, ".", 3) | ||
| if len(parts) != 3 { | ||
| return errors.New("invalid JWT format") | ||
| } | ||
|
|
||
| // The signed content is the raw "header.payload" string (not decoded) | ||
| signedContent := []byte(parts[0] + "." + parts[1]) | ||
|
|
||
| // Decode signature | ||
| sigBytes, err := base64.RawURLEncoding.DecodeString(parts[2]) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to decode signature: %w", err) | ||
| } | ||
|
|
||
| // Verify signature | ||
| if err := mldsa.Verify(pubKey, signedContent, sigBytes, nil); err != nil { | ||
| return errors.New("ML-DSA signature verification failed") | ||
| } | ||
|
|
||
| // Decode and validate claims | ||
| payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to decode payload: %w", err) | ||
| } | ||
| var claims jwtClaims | ||
| if err := json.Unmarshal(payloadBytes, &claims); err != nil { | ||
| return fmt.Errorf("failed to parse claims: %w", err) | ||
| } | ||
|
|
||
| // Check expiration | ||
| if int64(claims.Exp) < time.Now().Unix() { | ||
therealpaulgg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return errors.New("token expired") | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.