-
Notifications
You must be signed in to change notification settings - Fork 0
/
sign.go
131 lines (118 loc) · 3.11 KB
/
sign.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
package token
import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"time"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"jdtw.dev/token/nonce"
pb "jdtw.dev/token/proto/token"
)
const header = "jdtw.dev/token/v1"
// Errors returned from unmarshal:
var (
ErrMissingID = errors.New("key missing ID")
ErrInvaidKeyLen = errors.New("invalid key length")
ErrMissingSubject = errors.New("key missing subject")
)
type SigningKey struct {
key *pb.SigningKey
}
func (s *SigningKey) ID() string {
return s.key.Id
}
func (s *SigningKey) String() string {
return protojson.Format(s.key)
}
// Generate an Ed25519 keypair for the given subject.
func GenerateKey(subject string) (*VerificationKey, *SigningKey, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
digest := sha256.Sum256([]byte(pub))
keyID := hex.EncodeToString(digest[:])
return &VerificationKey{&pb.VerificationKey{
Id: keyID,
Subject: subject,
PublicKey: []byte(pub),
}},
&SigningKey{&pb.SigningKey{
Id: keyID,
PrivateKey: []byte(priv)},
},
nil
}
// UnmarshalSigningKey unmarshals a signing key from a binary proto.
func UnmarshalSigningKey(serialized []byte) (*SigningKey, error) {
key := &pb.SigningKey{}
if err := proto.Unmarshal(serialized, key); err != nil {
return nil, err
}
if key.Id == "" {
return nil, ErrMissingID
}
if len(key.PrivateKey) != ed25519.PrivateKeySize {
return nil, ErrInvaidKeyLen
}
return &SigningKey{key}, nil
}
func (k *SigningKey) Marshal() ([]byte, error) {
return proto.Marshal(k.key)
}
type SignOptions struct {
// The resource this token will be used for.
Resource string
// The current time. If zero, the current time will be used.
Now time.Time
// How long the token should be valid for.
Lifetime time.Duration
}
// Sign a token. Returns the signed token and its unique identifier as a hex encoded string.
func (k *SigningKey) Sign(opts *SignOptions) ([]byte, string, error) {
if opts.Resource == "" {
return nil, "", fmt.Errorf("token missing required resource")
}
if opts.Lifetime <= time.Duration(0) {
return nil, "", fmt.Errorf("token lifetime must be greater than zero")
}
now := opts.Now
if now.IsZero() {
now = time.Now()
}
nonce, err := nonce.New()
if err != nil {
return nil, "", err
}
bytes, err := proto.Marshal(&pb.Token{
Resource: opts.Resource,
NotBefore: timestamppb.New(now),
NotAfter: timestamppb.New(now.Add(opts.Lifetime)),
Nonce: nonce,
})
if err != nil {
return nil, "", err
}
priv := ed25519.PrivateKey(k.key.PrivateKey)
// Append the header before signing to prevent any sort of cross-protocol tomfoolery.
toSign := append([]byte(header), bytes...)
sig, err := priv.Sign(rand.Reader, toSign, crypto.Hash(0))
if err != nil {
return nil, "", err
}
bytes, err = proto.Marshal(&pb.SignedToken{
KeyId: k.key.Id,
Signature: sig,
Token: bytes,
})
if err != nil {
return nil, "", err
}
return bytes, hex.EncodeToString(nonce), nil
}