-
Notifications
You must be signed in to change notification settings - Fork 0
/
operations.go
299 lines (258 loc) · 9.19 KB
/
operations.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// Package ion provides all the functionality you need to interact with an ION service and manage your ION DID.
// To start, create a new ION resolution object using the NewResolver function. This will create a new resolution
// that can resolve and anchor ION DIDs. Next, create a new ION DID using the NewIONDID function. This will
// create a new ION DID object with a set of receiver methods that can be used to generate operations to submit
// to the ION service.
// For example:
// // Create a new ION resolution
// resolution, err := ion.NewResolver(http.DefaultClient, "https://ion.tbd.network")
//
// if err != nil {
// panic(err)
// }
//
// // Create a new ION DID
// did, createOp, err := ion.NewIONDID(Document{[]Service{Service{ID: "serviceID", Type: "serviceType"}}})
//
// if err != nil {
// panic(err)
// }
//
// // Submit the create operation to the ION service
// err = resolution.Anchor(ctx, createOp)
//
// if err != nil {
// panic(err)
// }
//
// // Resolve the DID
// result, err := resolution.Resolve(ctx, did, nil)
//
// if err != nil {
// panic(err)
// }
package ion
import (
"reflect"
"strings"
"github.com/cyware/ssi-sdk/crypto"
"github.com/cyware/ssi-sdk/crypto/jwx"
"github.com/cyware/ssi-sdk/did"
"github.com/cyware/ssi-sdk/util"
"github.com/pkg/errors"
)
type (
ION string
)
const (
Prefix = "did:ion"
)
// IsValid checks if the did:ion is valid by checking for a valid prefix
// full validation is impossible without resolution
func (d ION) IsValid() bool {
split := strings.Split(d.String(), Prefix+":")
return len(split) == 2
}
func (d ION) String() string {
return string(d)
}
func (d ION) Suffix() (string, error) {
split := strings.Split(d.String(), Prefix+":")
if len(split) != 2 {
return "", errors.Wrap(util.InvalidFormatError, "did is malformed")
}
return split[1], nil
}
func (ION) Method() did.Method {
return did.IONMethod
}
// DID is a representation of a did:ion DID and should be used to maintain the state of an ION
// DID Document. It contains the DID suffix, the long form DID, the operations of the DID, and both
// the update and recovery private keys. All receiver methods are side effect free, and return new
// instances of DID with the updated state.
type DID struct {
id string
suffix string
longFormDID string
operations []any
updatePrivateKey jwx.PrivateKeyJWK
recoveryPrivateKey jwx.PrivateKeyJWK
}
func (d DID) IsEmpty() bool {
return reflect.DeepEqual(d, DID{})
}
func (d DID) ID() string {
return d.id
}
func (d DID) LongForm() string {
return d.longFormDID
}
func (d DID) Operations() []any {
return d.operations
}
func (d DID) Operation(index int) any {
return d.operations[index]
}
func (d DID) GetUpdatePrivateKey() jwx.PrivateKeyJWK {
return d.updatePrivateKey
}
func (d DID) GetRecoveryPrivateKey() jwx.PrivateKeyJWK {
return d.recoveryPrivateKey
}
// NewIONDID creates a new ION DID with a new recovery and update key pairs, of type secp256k1, in addition
// to any content passed into in the document parameter. The result is a DID object that contains the long form DID,
// and operations to be submitted to an anchor service.
func NewIONDID(doc Document) (*DID, *CreateRequest, error) {
if doc.IsEmpty() {
return nil, nil, errors.New("document cannot be empty")
}
// generate update key pair
_, updatePrivateKey, err := crypto.GenerateSECP256k1Key()
if err != nil {
return nil, nil, errors.Wrap(err, "generating update keypair")
}
updatePubKeyJWK, updatePrivKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(nil, updatePrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "converting update key pair to JWK")
}
// generate recovery key pair
_, recoveryPrivateKey, err := crypto.GenerateSECP256k1Key()
if err != nil {
return nil, nil, errors.Wrap(err, "generating recovery keypair")
}
recoveryPubKeyJWK, recoveryPrivKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(nil, recoveryPrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "converting recovery keypair to JWK")
}
createRequest, err := NewCreateRequest(*recoveryPubKeyJWK, *updatePubKeyJWK, doc)
if err != nil {
return nil, nil, errors.Wrap(err, "generating create request")
}
longFormDID, err := CreateLongFormDID(*recoveryPubKeyJWK, *updatePubKeyJWK, doc)
if err != nil {
return nil, nil, errors.Wrap(err, "generating long form DID")
}
shortFormDID, err := LongToShortFormDID(longFormDID)
if err != nil {
return nil, nil, errors.Wrap(err, "generating short form DID")
}
suffix := shortFormDID[len("did:ion:"):]
return &DID{
id: shortFormDID,
suffix: suffix,
longFormDID: longFormDID,
operations: []any{createRequest},
updatePrivateKey: *updatePrivKeyJWK,
recoveryPrivateKey: *recoveryPrivKeyJWK,
}, createRequest, nil
}
// Update updates the DID object's state with a provided state change object. The result is a new DID object
// with the update key pair and an update operation to be submitted to an anchor service.
func (d DID) Update(stateChange StateChange) (*DID, *UpdateRequest, error) {
if d.IsEmpty() {
return nil, nil, errors.New("DID cannot be empty")
}
if err := stateChange.IsValid(); err != nil {
return nil, nil, errors.Wrap(err, "invalid state change")
}
// generate next update key pair
_, nextUpdatePrivateKey, err := crypto.GenerateSECP256k1Key()
if err != nil {
return nil, nil, errors.Wrap(err, "generating next update keypair")
}
nextUpdatePubKeyJWK, nextUpdatePrivKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(nil, nextUpdatePrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "converting next update key pair to JWK")
}
// create a signer with the current update key
signer, err := NewBTCSignerVerifier(d.updatePrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "creating signer")
}
updateRequest, err := NewUpdateRequest(d.suffix, d.updatePrivateKey.ToPublicKeyJWK(), *nextUpdatePubKeyJWK, *signer, stateChange)
if err != nil {
return nil, nil, errors.Wrap(err, "generating update request")
}
updatedDID := DID{
id: d.id,
suffix: d.suffix,
longFormDID: d.longFormDID,
operations: append(d.operations, updateRequest),
updatePrivateKey: *nextUpdatePrivKeyJWK,
recoveryPrivateKey: d.recoveryPrivateKey,
}
return &updatedDID, updateRequest, nil
}
// Recover recovers the DID object's state with a provided document object, returning a new DID object and
// recover operation to be submitted to an anchor service.
func (d DID) Recover(doc Document) (*DID, *RecoverRequest, error) {
if d.IsEmpty() {
return nil, nil, errors.New("DID cannot be empty")
}
if doc.IsEmpty() {
return nil, nil, errors.New("document cannot be empty")
}
// generate next recovery key pair
_, nextRecoveryPrivateKey, err := crypto.GenerateSECP256k1Key()
if err != nil {
return nil, nil, errors.Wrap(err, "generating nest recovery keypair")
}
nextRecoveryPubKeyJWK, nextRecoveryPrivKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(nil, nextRecoveryPrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "converting next recovery key pair to JWK")
}
// generate next update key pair
_, nextUpdatePrivateKey, err := crypto.GenerateSECP256k1Key()
if err != nil {
return nil, nil, errors.Wrap(err, "generating next update keypair")
}
nextUpdatePubKeyJWK, nextUpdatePrivKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(nil, nextUpdatePrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "converting next update key pair to JWK")
}
// create a signer with the current update key
signer, err := NewBTCSignerVerifier(d.updatePrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "creating signer")
}
recoverRequest, err := NewRecoverRequest(d.suffix, d.recoveryPrivateKey.ToPublicKeyJWK(), *nextRecoveryPubKeyJWK, *nextUpdatePubKeyJWK, doc, *signer)
if err != nil {
return nil, nil, errors.Wrap(err, "generating recover request")
}
recoveredDID := DID{
id: d.id,
suffix: d.suffix,
longFormDID: d.longFormDID,
operations: append(d.operations, recoverRequest),
updatePrivateKey: *nextUpdatePrivKeyJWK,
recoveryPrivateKey: *nextRecoveryPrivKeyJWK,
}
return &recoveredDID, recoverRequest, nil
}
// Deactivate creates a terminal state DID and the corresponding anchor operation to submit to the anchor service.
func (d DID) Deactivate() (*DID, *DeactivateRequest, error) {
if d.IsEmpty() {
return nil, nil, errors.New("DID cannot be empty")
}
// create a signer with the current update key
signer, err := NewBTCSignerVerifier(d.updatePrivateKey)
if err != nil {
return nil, nil, errors.Wrap(err, "creating signer")
}
deactivateRequest, err := NewDeactivateRequest(d.suffix, d.updatePrivateKey.ToPublicKeyJWK(), *signer)
if err != nil {
return nil, nil, errors.Wrap(err, "generating deactivate request")
}
deactivatedDID := DID{
id: d.id,
suffix: d.suffix,
longFormDID: d.longFormDID,
operations: append(d.operations, deactivateRequest),
updatePrivateKey: d.updatePrivateKey,
recoveryPrivateKey: d.recoveryPrivateKey,
}
return &deactivatedDID, deactivateRequest, nil
}
func is2xxStatusCode(statusCode int) bool {
return statusCode >= 200 && statusCode < 300
}