1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package pdsclient
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
const dpopProofLifetime = 30 * time.Second
func DPoPSourceFromData(data map[string]any) (DPoPSource, error) {
key, err := dpopKey(data)
if err != nil {
return nil, err
}
return &dpopSigner{key: key}, nil
}
func dpopKey(data map[string]any) (atcrypto.PrivateKeyExportable, error) {
if value, ok := data["dpopPrivateJwk"]; ok {
key, err := dpopKeyValue(value)
if err == nil {
return key, nil
}
}
for _, name := range []string{"dpopPrivateKeyMultibase", "dpop_privatekey_multibase", "privateKeyMultibase", "private_key_multibase", "dpopPrivateKey", "privateKey", "key", "value"} {
value, ok := data[name]
if !ok {
continue
}
key, err := dpopKeyValue(value)
if err == nil {
return key, nil
}
}
return nil, fmt.Errorf("%w: the dpop key ref holds no private key", ErrInvalidConfig)
}
func dpopKeyValue(value any) (atcrypto.PrivateKeyExportable, error) {
switch typed := value.(type) {
case map[string]any:
d, _ := typed["d"].(string)
if d == "" {
return nil, fmt.Errorf("%w: the dpop jwk has no d", ErrInvalidConfig)
}
return dpopKeyP256(d)
case string:
raw := strings.TrimSpace(typed)
if raw == "" {
return nil, fmt.Errorf("%w: the dpop key is empty", ErrInvalidConfig)
}
if strings.HasPrefix(raw, "{") {
var jwk map[string]any
if err := json.Unmarshal([]byte(raw), &jwk); err != nil {
return nil, fmt.Errorf("%w: the dpop jwk: %v", ErrInvalidConfig, err)
}
return dpopKeyValue(jwk)
}
if strings.HasPrefix(raw, "z") {
key, err := atcrypto.ParsePrivateMultibase(raw)
if err != nil {
return nil, fmt.Errorf("%w: the dpop multibase key: %v", ErrInvalidConfig, err)
}
return key, nil
}
return dpopKeyP256(raw)
}
return nil, fmt.Errorf("%w: unsupported dpop key value", ErrInvalidConfig)
}
func dpopKeyP256(raw string) (atcrypto.PrivateKeyExportable, error) {
decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(strings.TrimSpace(raw), "="))
if err != nil {
return nil, fmt.Errorf("%w: the dpop key: %v", ErrInvalidConfig, err)
}
key, err := atcrypto.ParsePrivateBytesP256(decoded)
if err != nil {
return nil, fmt.Errorf("%w: the dpop key: %v", ErrInvalidConfig, err)
}
return key, nil
}
type dpopSigner struct {
key atcrypto.PrivateKeyExportable
}
func (s *dpopSigner) DPoPProof(ctx context.Context, method, target, accessToken string) (string, error) {
if s == nil || s.key == nil {
return "", fmt.Errorf("%w: no dpop key", ErrInvalidConfig)
}
parsed, err := url.Parse(target)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("%w: the dpop target %q is not absolute", ErrInvalidRequest, target)
}
public, err := s.key.PublicKey()
if err != nil {
return "", err
}
jwk, err := public.JWK()
if err != nil {
return "", err
}
header, err := json.Marshal(struct {
Typ string `json:"typ"`
Alg string `json:"alg"`
JWK *atcrypto.JWK `json:"jwk"`
}{Typ: "dpop+jwt", Alg: "ES256", JWK: jwk})
if err != nil {
return "", err
}
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); err != nil {
return "", err
}
htu := *parsed
htu.RawQuery = ""
htu.Fragment = ""
now := time.Now()
claims := map[string]any{
"jti": base64.RawURLEncoding.EncodeToString(nonce),
"htm": strings.ToUpper(method),
"htu": htu.String(),
"iat": now.Unix(),
"exp": now.Add(dpopProofLifetime).Unix(),
}
if accessToken != "" {
sum := sha256.Sum256([]byte(accessToken))
claims["ath"] = base64.RawURLEncoding.EncodeToString(sum[:])
}
payload, err := json.Marshal(claims)
if err != nil {
return "", err
}
signingInput := base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload)
signature, err := s.key.HashAndSign([]byte(signingInput))
if err != nil {
return "", err
}
return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature), nil
}