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