forked from SherClockHolmes/webpush-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvapid.go
177 lines (156 loc) · 4.67 KB
/
vapid.go
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package webpush
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net/url"
"strings"
"time"
jwt "github.com/golang-jwt/jwt/v5"
)
// VAPIDKeys is a public-private keypair for use in VAPID.
// It marshals to a JSON string containing the PEM of the PKCS8
// of the private key.
type VAPIDKeys struct {
privateKey *ecdsa.PrivateKey
publicKey string // raw bytes encoding in urlsafe base64, as per RFC
}
// PublicKeyString returns the base64url-encoded uncompressed public key of the keypair,
// as defined in RFC8292.
func (v *VAPIDKeys) PublicKeyString() string {
return v.publicKey
}
// PrivateKey returns the private key of the keypair.
func (v *VAPIDKeys) PrivateKey() *ecdsa.PrivateKey {
return v.privateKey
}
// Equal compares two VAPIDKeys for equality.
func (v *VAPIDKeys) Equal(o *VAPIDKeys) bool {
return v.privateKey.Equal(o.privateKey)
}
var _ json.Marshaler = (*VAPIDKeys)(nil)
var _ json.Unmarshaler = (*VAPIDKeys)(nil)
// MarshalJSON implements json.Marshaler, allowing serialization to JSON.
func (v *VAPIDKeys) MarshalJSON() ([]byte, error) {
pkcs8bytes, err := x509.MarshalPKCS8PrivateKey(v.privateKey)
if err != nil {
return nil, err
}
pemBlock := pem.Block{
Type: "PRIVATE KEY",
Bytes: pkcs8bytes,
}
pemBytes := pem.EncodeToMemory(&pemBlock)
if pemBytes == nil {
return nil, fmt.Errorf("could not encode VAPID keys as PEM")
}
return json.Marshal(string(pemBytes))
}
// MarshalJSON implements json.Unmarshaler, allowing deserialization from JSON.
func (v *VAPIDKeys) UnmarshalJSON(b []byte) error {
var pemKey string
if err := json.Unmarshal(b, &pemKey); err != nil {
return err
}
pemBlock, _ := pem.Decode([]byte(pemKey))
if pemBlock == nil {
return fmt.Errorf("could not decode PEM block with VAPID keys")
}
privKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
if err != nil {
return err
}
privateKey, ok := privKey.(*ecdsa.PrivateKey)
if !ok {
return fmt.Errorf("Invalid type of private key %T", privateKey)
}
if privateKey.Curve != elliptic.P256() {
return fmt.Errorf("Invalid curve for private key %v", privateKey.Curve)
}
publicKeyStr, err := makePublicKeyString(privateKey)
if err != nil {
return err // should not be possible since we confirmed P256 already
}
// success
v.privateKey = privateKey
v.publicKey = publicKeyStr
return nil
}
// GenerateVAPIDKeys generates a VAPID keypair (an ECDSA keypair on
// the P-256 curve).
func GenerateVAPIDKeys() (result *VAPIDKeys, err error) {
private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return
}
pubKeyECDH, err := private.PublicKey.ECDH()
if err != nil {
return
}
publicKey := base64.RawURLEncoding.EncodeToString(pubKeyECDH.Bytes())
return &VAPIDKeys{
privateKey: private,
publicKey: publicKey,
}, nil
}
// ECDSAToVAPIDKeys wraps an existing ecdsa.PrivateKey in VAPIDKeys for use in
// VAPID header signing.
func ECDSAToVAPIDKeys(privKey *ecdsa.PrivateKey) (result *VAPIDKeys, err error) {
if privKey.Curve != elliptic.P256() {
return nil, fmt.Errorf("Invalid curve for private key %v", privKey.Curve)
}
publicKeyString, err := makePublicKeyString(privKey)
if err != nil {
return nil, err
}
return &VAPIDKeys{
privateKey: privKey,
publicKey: publicKeyString,
}, nil
}
func makePublicKeyString(privKey *ecdsa.PrivateKey) (result string, err error) {
// to get the raw bytes we have to convert the public key to *ecdh.PublicKey
// this type assertion (from the crypto.PublicKey returned by (*ecdsa.PrivateKey).Public()
// to *ecdsa.PublicKey) cannot fail:
publicKey, err := privKey.Public().(*ecdsa.PublicKey).ECDH()
if err != nil {
return // should not be possible if we confirmed P256 already
}
return base64.RawURLEncoding.EncodeToString(publicKey.Bytes()), nil
}
// getVAPIDAuthorizationHeader
func getVAPIDAuthorizationHeader(
endpoint string,
subscriber string,
vapidKeys *VAPIDKeys,
expiration time.Time,
) (string, error) {
if expiration.IsZero() {
expiration = time.Now().Add(time.Hour * 12)
}
// Create the JWT token
subURL, err := url.Parse(endpoint)
if err != nil {
return "", err
}
// Unless subscriber is an HTTPS URL, assume an e-mail address
if !strings.HasPrefix(subscriber, "https:") && !strings.HasPrefix(subscriber, "mailto:") {
subscriber = "mailto:" + subscriber
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"aud": subURL.Scheme + "://" + subURL.Host,
"exp": expiration.Unix(),
"sub": subscriber,
})
// Sign token with private key
jwtString, err := token.SignedString(vapidKeys.privateKey)
if err != nil {
return "", err
}
return "vapid t=" + jwtString + ", k=" + vapidKeys.publicKey, nil
}