Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

feat: add DPoP ES256 key generation and proof JWT builder

+119 -1
+1 -1
go.mod
··· 3 3 go 1.22 4 4 5 5 require ( 6 + github.com/golang-jwt/jwt/v5 v5.3.1 6 7 github.com/gorilla/sessions v1.2.2 7 8 github.com/mattn/go-sqlite3 v1.14.22 8 9 github.com/oklog/ulid/v2 v2.1.0 ··· 15 16 require ( 16 17 cloud.google.com/go/compute v1.20.1 // indirect 17 18 cloud.google.com/go/compute/metadata v0.2.3 // indirect 18 - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect 19 19 github.com/gorilla/securecookie v1.1.2 // indirect 20 20 )
+118
internal/atproto/dpop/dpop.go
··· 1 + package dpop 2 + 3 + import ( 4 + "crypto/ecdsa" 5 + "crypto/elliptic" 6 + "crypto/rand" 7 + "encoding/base64" 8 + "encoding/json" 9 + "fmt" 10 + "math/big" 11 + "time" 12 + 13 + "github.com/golang-jwt/jwt/v5" 14 + ) 15 + 16 + // KeyPair holds an ES256 key pair for DPoP use. 17 + type KeyPair struct { 18 + Private *ecdsa.PrivateKey 19 + } 20 + 21 + // Generate creates a new ES256 key pair. 22 + func Generate() (*KeyPair, error) { 23 + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 24 + if err != nil { 25 + return nil, fmt.Errorf("generate DPoP key: %w", err) 26 + } 27 + return &KeyPair{Private: priv}, nil 28 + } 29 + 30 + // PublicJWK returns the public key as a JWK map (for embedding in DPoP JWT header). 31 + func (kp *KeyPair) PublicJWK() map[string]interface{} { 32 + pub := kp.Private.PublicKey 33 + return map[string]interface{}{ 34 + "kty": "EC", 35 + "crv": "P-256", 36 + "x": base64.RawURLEncoding.EncodeToString(pub.X.Bytes()), 37 + "y": base64.RawURLEncoding.EncodeToString(pub.Y.Bytes()), 38 + } 39 + } 40 + 41 + // MarshalPrivate serializes the private key to JWK JSON for session storage. 42 + func (kp *KeyPair) MarshalPrivate() ([]byte, error) { 43 + pub := kp.Private.PublicKey 44 + jwk := map[string]interface{}{ 45 + "kty": "EC", 46 + "crv": "P-256", 47 + "x": base64.RawURLEncoding.EncodeToString(pub.X.Bytes()), 48 + "y": base64.RawURLEncoding.EncodeToString(pub.Y.Bytes()), 49 + "d": base64.RawURLEncoding.EncodeToString(kp.Private.D.Bytes()), 50 + } 51 + return json.Marshal(jwk) 52 + } 53 + 54 + // UnmarshalPrivate deserializes a private key from JWK JSON. 55 + func UnmarshalPrivate(data []byte) (*KeyPair, error) { 56 + var jwk map[string]json.RawMessage 57 + if err := json.Unmarshal(data, &jwk); err != nil { 58 + return nil, err 59 + } 60 + 61 + decode := func(key string) ([]byte, error) { 62 + var s string 63 + if err := json.Unmarshal(jwk[key], &s); err != nil { 64 + return nil, err 65 + } 66 + return base64.RawURLEncoding.DecodeString(s) 67 + } 68 + 69 + xb, err := decode("x") 70 + if err != nil { 71 + return nil, err 72 + } 73 + yb, err := decode("y") 74 + if err != nil { 75 + return nil, err 76 + } 77 + db, err := decode("d") 78 + if err != nil { 79 + return nil, err 80 + } 81 + 82 + priv := &ecdsa.PrivateKey{ 83 + PublicKey: ecdsa.PublicKey{ 84 + Curve: elliptic.P256(), 85 + X: new(big.Int).SetBytes(xb), 86 + Y: new(big.Int).SetBytes(yb), 87 + }, 88 + D: new(big.Int).SetBytes(db), 89 + } 90 + return &KeyPair{Private: priv}, nil 91 + } 92 + 93 + // randJTI generates a random 16-byte hex string for use as the JWT ID. 94 + func randJTI() string { 95 + b := make([]byte, 16) 96 + rand.Read(b) 97 + return fmt.Sprintf("%x", b) 98 + } 99 + 100 + // Proof builds a DPoP proof JWT for the given HTTP method and URL. 101 + // nonce is optional (include when the server has issued one). 102 + func (kp *KeyPair) Proof(method, htu, nonce string) (string, error) { 103 + claims := jwt.MapClaims{ 104 + "jti": randJTI(), 105 + "htm": method, 106 + "htu": htu, 107 + "iat": time.Now().Unix(), 108 + } 109 + if nonce != "" { 110 + claims["nonce"] = nonce 111 + } 112 + 113 + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 114 + token.Header["typ"] = "dpop+jwt" 115 + token.Header["jwk"] = kp.PublicJWK() 116 + 117 + return token.SignedString(kp.Private) 118 + }