github.com/hyperledger/aries-framework-go@v0.3.2/pkg/didcomm/protocol/outofbandv2/service.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package outofbandv2 8 9 import ( 10 "encoding/json" 11 "errors" 12 "fmt" 13 "strings" 14 15 gojose "github.com/go-jose/go-jose/v3" 16 "github.com/google/uuid" 17 18 "github.com/hyperledger/aries-framework-go/pkg/common/log" 19 "github.com/hyperledger/aries-framework-go/pkg/common/model" 20 "github.com/hyperledger/aries-framework-go/pkg/crypto" 21 "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" 22 "github.com/hyperledger/aries-framework-go/pkg/didcomm/dispatcher" 23 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" 24 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator" 25 "github.com/hyperledger/aries-framework-go/pkg/didcomm/transport" 26 "github.com/hyperledger/aries-framework-go/pkg/doc/did" 27 "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" 28 "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" 29 vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" 30 "github.com/hyperledger/aries-framework-go/pkg/internal/logutil" 31 "github.com/hyperledger/aries-framework-go/pkg/kms" 32 "github.com/hyperledger/aries-framework-go/pkg/store/connection" 33 "github.com/hyperledger/aries-framework-go/pkg/vdr/peer" 34 "github.com/hyperledger/aries-framework-go/spi/storage" 35 ) 36 37 const ( 38 // Name of this protocol service. 39 Name = "out-of-band/2.0" 40 // dbName is the name of this service's db stores. 41 dbName = "_OutOfBand2" 42 // PIURI is the Out-of-Band protocol's protocol instance URI. 43 PIURI = "https://didcomm.org/" + Name 44 // InvitationMsgType is the '@type' for the invitation message. 45 InvitationMsgType = PIURI + "/invitation" 46 47 // TODO channel size - https://github.com/hyperledger/aries-framework-go/issues/246 48 callbackChannelSize = 10 49 50 contextKey = "context_%s" 51 52 ed25519VerificationKey2018 = "Ed25519VerificationKey2018" 53 bls12381G2Key2020 = "Bls12381G2Key2020" 54 jsonWebKey2020 = "JsonWebKey2020" 55 x25519KeyAgreementKey2019 = "X25519KeyAgreementKey2019" 56 ) 57 58 var logger = log.New(fmt.Sprintf("aries-framework/%s/service", Name)) 59 60 // Service implements the Out-Of-Band V2 protocol. 61 type Service struct { 62 service.Action 63 service.Message 64 vdrRegistry vdrapi.Registry 65 callbackChannel chan *callback 66 transientStore storage.Store 67 connectionRecorder *connection.Recorder 68 inboundHandler func() service.InboundHandler 69 listenerFunc func() 70 messenger service.Messenger 71 myMediaTypeProfiles []string 72 kms kms.KeyManager 73 keyType kms.KeyType 74 keyAgreementType kms.KeyType 75 msgTypeServicesTargets map[string]string 76 allServices []dispatcher.ProtocolService 77 routeSvc mediator.ProtocolService 78 initialized bool 79 } 80 81 type callback struct { 82 msg service.DIDCommMsg 83 } 84 85 // Provider provides this service's dependencies. 86 type Provider interface { 87 Service(id string) (interface{}, error) 88 StorageProvider() storage.Provider 89 VDRegistry() vdrapi.Registry 90 ProtocolStateStorageProvider() storage.Provider 91 InboundDIDCommMessageHandler() func() service.InboundHandler 92 Messenger() service.Messenger 93 KMS() kms.KeyManager 94 KeyType() kms.KeyType 95 KeyAgreementType() kms.KeyType 96 MediaTypeProfiles() []string 97 ServiceMsgTypeTargets() []dispatcher.MessageTypeTarget 98 AllServices() []dispatcher.ProtocolService 99 } 100 101 // New creates a new instance of the out-of-band service. 102 func New(p Provider) (*Service, error) { 103 svc := Service{} 104 105 err := svc.Initialize(p) 106 if err != nil { 107 return nil, err 108 } 109 110 return &svc, nil 111 } 112 113 // Initialize initializes the Service. If Initialize succeeds, any further call is a no-op. 114 func (s *Service) Initialize(prov interface{}) error { // nolint:funlen 115 if s.initialized { 116 return nil 117 } 118 119 p, ok := prov.(Provider) 120 if !ok { 121 return fmt.Errorf("oob/2.0 expected provider of type `%T`, got type `%T`", Provider(nil), p) 122 } 123 124 store, err := p.ProtocolStateStorageProvider().OpenStore(dbName) 125 if err != nil { 126 return fmt.Errorf("oob/2.0 failed to open the transientStore : %w", err) 127 } 128 129 err = p.ProtocolStateStorageProvider().SetStoreConfig(dbName, 130 storage.StoreConfiguration{TagNames: []string{contextKey}}) 131 if err != nil { 132 return fmt.Errorf("oob/2.0 failed to set transientStore config in protocol state transientStore: %w", err) 133 } 134 135 msgTypeServicesTargets := map[string]string{} 136 137 for _, v := range p.ServiceMsgTypeTargets() { 138 msgTypeServicesTargets[v.Target] = v.MsgType 139 } 140 141 connRecorder, err := connection.NewRecorder(p) 142 if err != nil { 143 return fmt.Errorf("failed to initialize connection recorder: %w", err) 144 } 145 146 routeSvcBase, err := p.Service(mediator.Coordination) 147 if err != nil { 148 return err 149 } 150 151 routeSvc, ok := routeSvcBase.(mediator.ProtocolService) 152 if !ok { 153 return errors.New("cast service to Route Service failed") 154 } 155 156 s.callbackChannel = make(chan *callback, callbackChannelSize) 157 s.transientStore = store 158 s.vdrRegistry = p.VDRegistry() 159 s.connectionRecorder = connRecorder 160 s.inboundHandler = p.InboundDIDCommMessageHandler() 161 s.messenger = p.Messenger() 162 s.myMediaTypeProfiles = p.MediaTypeProfiles() 163 s.msgTypeServicesTargets = msgTypeServicesTargets 164 s.kms = p.KMS() 165 s.keyType = p.KeyType() 166 s.keyAgreementType = p.KeyAgreementType() 167 s.allServices = p.AllServices() 168 s.listenerFunc = listener(s.callbackChannel, s.handleCallback) 169 s.routeSvc = routeSvc 170 171 go s.listenerFunc() 172 173 s.initialized = true 174 175 return nil 176 } 177 178 // Name is this service's name. 179 func (s *Service) Name() string { 180 return Name 181 } 182 183 // Accept determines whether this service can handle the given type of message. 184 func (s *Service) Accept(msgType string) bool { 185 return msgType == InvitationMsgType 186 } 187 188 // HandleInbound handles inbound messages. 189 func (s *Service) HandleInbound(msg service.DIDCommMsg, didCommCtx service.DIDCommContext) (string, error) { 190 logger.Debugf("oob/2.0 inbound message: %s", msg) 191 192 if msg == nil { 193 return "", fmt.Errorf("oob/2.0 cannot handle nil inbound message") 194 } 195 196 if !s.Accept(msg.Type()) { 197 return "", fmt.Errorf("oob/2.0 unsupported message type %s", msg.Type()) 198 } 199 200 return "", nil 201 } 202 203 // HandleOutbound handles outbound messages. 204 func (s *Service) HandleOutbound(_ service.DIDCommMsg, _, _ string) (string, error) { 205 // TODO implement 206 return "", errors.New("oob/2.0 not implemented") 207 } 208 209 type acceptOpts struct { 210 routerConnections []string 211 } 212 213 // AcceptOption boilerplate adds configured parameter to Service.AcceptInvitation. 214 type AcceptOption func(opts *acceptOpts) 215 216 // WithRouterConnections option sets router connections for the connection created with this Invitation acceptance. 217 func WithRouterConnections(routerConnections []string) AcceptOption { 218 return func(opts *acceptOpts) { 219 opts.routerConnections = routerConnections 220 } 221 } 222 223 // AcceptInvitation from another agent. 224 //nolint:funlen,gocyclo 225 func (s *Service) AcceptInvitation(i *Invitation, opts ...AcceptOption) (string, error) { 226 options := &acceptOpts{} 227 228 for _, opt := range opts { 229 opt(options) 230 } 231 232 msg := service.NewDIDCommMsgMap(i) 233 234 err := validateInvitationAcceptance(msg, s.myMediaTypeProfiles) 235 if err != nil { 236 return "", fmt.Errorf("oob/2.0 unable to accept invitation: %w", err) 237 } 238 239 if i.From == "" { 240 return "", fmt.Errorf("oob/2.0 does not have from field") 241 } 242 243 clbk := &callback{ 244 msg: msg, 245 } 246 247 err = s.handleCallback(clbk) 248 if err != nil { 249 return "", fmt.Errorf("oob/2.0 failed to accept invitation : %w", err) 250 } 251 252 newDID := &did.Doc{} 253 254 err = s.createNewKeyAndVM(newDID) 255 if err != nil { 256 return "", fmt.Errorf("oob/2.0 AcceptInvitation: creating new keys and VMS for DID document failed: %w", err) 257 } 258 259 recKey := newDID.KeyAgreement[0].VerificationMethod.ID 260 261 services := []did.Service{} 262 263 for _, connID := range options.routerConnections { 264 // get the route configs (pass empty service endpoint, as default service endpoint added in VDR) 265 serviceEndpoint, routingKeys, e := mediator.GetRouterConfig(s.routeSvc, connID, "") 266 if e != nil { 267 return "", fmt.Errorf("did doc - fetch router config: %w", e) 268 } 269 270 services = append(services, did.Service{ 271 ServiceEndpoint: model.NewDIDCommV2Endpoint([]model.DIDCommV2Endpoint{ 272 {URI: serviceEndpoint, Accept: s.myMediaTypeProfiles, RoutingKeys: routingKeys}, 273 }), 274 RecipientKeys: []string{recKey}, 275 Type: vdrapi.DIDCommV2ServiceType, 276 }) 277 } 278 279 if len(services) == 0 { 280 services = []did.Service{ 281 { 282 RecipientKeys: []string{recKey}, 283 Type: vdrapi.DIDCommV2ServiceType, 284 }, 285 } 286 } 287 288 newDID.Service = services 289 290 if i.Body != nil && i.Body.GoalCode != "" { 291 serviceURL := s.msgTypeServicesTargets[i.Body.GoalCode] 292 for _, srvc := range s.allServices { 293 if strings.Contains(serviceURL, srvc.Name()) { 294 connID := s.handleInboundService(serviceURL, srvc, i.From, i.Requests, newDID) 295 296 if connID != "" { 297 logger.Debugf("oob/2.0 matching target service found for url '%v' and executed, "+ 298 "oobv2.AcceptInvitation() is done.", serviceURL) 299 return connID, nil 300 } 301 } 302 } 303 304 logger.Debugf("oob/2.0 no matching target service found for url '%v', oobv2.AcceptInvitation() is done but"+ 305 " no target service triggered", serviceURL) 306 } 307 308 logger.Debugf("oob/2.0 request body or Goal code is empty, oobv2.AcceptInvitation() is done but no" + 309 "target service triggered, generating a new peer DID for the first valid attachment and return it") 310 311 senderDoc, err := s.vdrRegistry.Resolve(i.From) 312 if err != nil { 313 return "", fmt.Errorf("oob/2.0 failed to resolve inviter DID: %w", err) 314 } 315 316 myDID, err := s.vdrRegistry.Create(peer.DIDMethod, newDID) 317 if err != nil { 318 return "", fmt.Errorf("oob/2.0 creating new DID via VDR failed: %w", err) 319 } 320 321 if len(options.routerConnections) != 0 { 322 err = s.addRouterKeys(myDID.DIDDocument, options.routerConnections) 323 if err != nil { 324 return "", err 325 } 326 } 327 328 destination, err := service.CreateDestination(senderDoc.DIDDocument) 329 if err != nil { 330 return "", fmt.Errorf("oob/2.0 failed to create destination: %w", err) 331 } 332 333 initialState, err := peer.UnsignedGenesisDelta(myDID.DIDDocument) 334 if err != nil { 335 return "", fmt.Errorf("marshalling peer DID into initialState: %w", err) 336 } 337 338 connRecord := &connection.Record{ 339 ConnectionID: uuid.New().String(), 340 ParentThreadID: i.ID, 341 State: connection.StateNameCompleted, 342 InvitationID: i.ID, 343 ServiceEndPoint: destination.ServiceEndpoint, 344 RecipientKeys: destination.RecipientKeys, 345 TheirLabel: i.Label, 346 TheirDID: i.From, 347 MyDID: myDID.DIDDocument.ID, 348 Namespace: connection.MyNSPrefix, 349 Implicit: true, 350 InvitationDID: myDID.DIDDocument.ID, 351 DIDCommVersion: service.V2, 352 PeerDIDInitialState: initialState, 353 } 354 355 if err := s.connectionRecorder.SaveConnectionRecord(connRecord); err != nil { 356 return "", fmt.Errorf("oob/2.0 failed to create connection: %w", err) 357 } 358 359 return connRecord.ConnectionID, nil 360 } 361 362 // TODO refactor: mostly copied from didexchange svc. 363 func (s *Service) addRouterKeys(doc *did.Doc, routerConnections []string) error { 364 // use KeyAgreement.ID as recKey for DIDComm V2 365 for _, ka := range doc.KeyAgreement { 366 kaID := ka.VerificationMethod.ID 367 if strings.HasPrefix(kaID, "#") { 368 kaID = doc.ID + kaID 369 } 370 371 for _, connID := range routerConnections { 372 // TODO https://github.com/hyperledger/aries-framework-go/issues/1105 add multiple keys at once. 373 if err := mediator.AddKeyToRouter(s.routeSvc, connID, kaID); err != nil { 374 return fmt.Errorf("did doc - add key to the router: %w", err) 375 } 376 } 377 } 378 379 return nil 380 } 381 382 // SaveInvitation saves OOB v2 Invitation. 383 func (s *Service) SaveInvitation(inv *Invitation) error { 384 return s.connectionRecorder.SaveOOBv2Invitation(inv.From, *inv) 385 } 386 387 func (s *Service) handleInboundService(serviceURL string, srvc dispatcher.ProtocolService, senderDID string, 388 attachments []*decorator.AttachmentV2, newDID *did.Doc) string { 389 for _, atchmnt := range attachments { 390 serviceRequest, err := atchmnt.Data.Fetch() 391 if err != nil { 392 logger.Debugf("oob/2.0 fetching target service '%v' for url '%v' attachment request failed:"+ 393 " %v, skipping attachment entry..", srvc.Name(), serviceURL, err) 394 395 continue 396 } 397 398 didCommMsgRequest := service.DIDCommMsgMap{} 399 400 err = didCommMsgRequest.UnmarshalJSON(serviceRequest) 401 if err != nil { 402 logger.Debugf("oob/2.0 fetching target service '%v' for url '%v' attachment request failed:"+ 403 " %v, skipping attachment entry..", srvc.Name(), serviceURL, err) 404 405 continue 406 } 407 408 myDID, err := s.vdrRegistry.Create(peer.DIDMethod, newDID) 409 if err != nil { 410 logger.Debugf("oob/2.0 fetching target service '%v' for url '%v' creating new DID via VDR "+ 411 "failed: %v, skipping attachment entry..", srvc.Name(), serviceURL, err) 412 413 continue 414 } 415 416 // TODO bug: most services don't return a connection ID from handleInbound, we can't expect it from there. 417 connID, err := srvc.HandleInbound(didCommMsgRequest, service.NewDIDCommContext(myDID.DIDDocument.ID, 418 senderDID, nil)) 419 if err != nil { 420 logger.Debugf("oob/2.0 executing target service '%v' for url '%v' failed: %v, skipping "+ 421 "attachment entry..", srvc.Name(), serviceURL, err) 422 423 continue 424 } 425 426 logger.Debugf("oob/2.0 successfully executed target service '%v' for target url: '%v', returned id: %v", 427 srvc.Name(), serviceURL, connID) 428 429 return connID 430 } 431 432 return "" 433 } 434 435 // TODO below function and sub functions are copied from pkg/didcomm/protocol/didexchange/keys.go 436 // move code in a common location and remove duplicate code. 437 func (s *Service) createNewKeyAndVM(didDoc *did.Doc) error { 438 vm, err := s.createSigningVM() 439 if err != nil { 440 return err 441 } 442 443 kaVM, err := s.createEncryptionVM() 444 if err != nil { 445 return err 446 } 447 448 didDoc.VerificationMethod = append(didDoc.VerificationMethod, *vm) 449 450 didDoc.Authentication = append(didDoc.Authentication, *did.NewReferencedVerification(vm, did.Authentication)) 451 didDoc.KeyAgreement = append(didDoc.KeyAgreement, *did.NewReferencedVerification(kaVM, did.KeyAgreement)) 452 453 return nil 454 } 455 456 // nolint:gochecknoglobals 457 var vmType = map[kms.KeyType]string{ 458 kms.ED25519Type: ed25519VerificationKey2018, 459 kms.BLS12381G2Type: bls12381G2Key2020, 460 kms.ECDSAP256TypeDER: jsonWebKey2020, 461 kms.ECDSAP256TypeIEEEP1363: jsonWebKey2020, 462 kms.ECDSAP384TypeDER: jsonWebKey2020, 463 kms.ECDSAP384TypeIEEEP1363: jsonWebKey2020, 464 kms.ECDSAP521TypeDER: jsonWebKey2020, 465 kms.ECDSAP521TypeIEEEP1363: jsonWebKey2020, 466 kms.X25519ECDHKWType: x25519KeyAgreementKey2019, 467 kms.NISTP256ECDHKWType: jsonWebKey2020, 468 kms.NISTP384ECDHKWType: jsonWebKey2020, 469 kms.NISTP521ECDHKWType: jsonWebKey2020, 470 } 471 472 func getVerMethodType(kt kms.KeyType) string { 473 return vmType[kt] 474 } 475 476 func (s *Service) createSigningVM() (*did.VerificationMethod, error) { 477 vmType := getVerMethodType(s.keyType) 478 479 _, pubKeyBytes, err := s.kms.CreateAndExportPubKeyBytes(s.keyType) 480 if err != nil { 481 return nil, fmt.Errorf("createSigningVM: %w", err) 482 } 483 484 vmID := "#key-1" 485 486 switch vmType { 487 case ed25519VerificationKey2018, bls12381G2Key2020: 488 return did.NewVerificationMethodFromBytes(vmID, vmType, "", pubKeyBytes), nil 489 case jsonWebKey2020: 490 j, err := jwksupport.PubKeyBytesToJWK(pubKeyBytes, s.keyType) 491 if err != nil { 492 return nil, fmt.Errorf("createSigningVM: failed to convert public key to JWK for VM: %w", err) 493 } 494 495 return did.NewVerificationMethodFromJWK(vmID, vmType, "", j) 496 default: 497 return nil, fmt.Errorf("createSigningVM: unsupported verification method: '%s'", vmType) 498 } 499 } 500 501 func (s *Service) createEncryptionVM() (*did.VerificationMethod, error) { 502 encKeyType := s.keyAgreementType 503 504 vmType := getVerMethodType(encKeyType) 505 506 _, kaPubKeyBytes, err := s.kms.CreateAndExportPubKeyBytes(encKeyType) 507 if err != nil { 508 return nil, fmt.Errorf("createEncryptionVM: %w", err) 509 } 510 511 vmID := "#key-2" 512 513 switch vmType { 514 case x25519KeyAgreementKey2019: 515 key := &crypto.PublicKey{} 516 517 err = json.Unmarshal(kaPubKeyBytes, key) 518 if err != nil { 519 return nil, fmt.Errorf("createEncryptionVM: unable to unmarshal X25519 key: %w", err) 520 } 521 522 return did.NewVerificationMethodFromBytes(vmID, vmType, "", key.X), nil 523 case jsonWebKey2020: 524 j, err := buildJWKFromBytes(kaPubKeyBytes) 525 if err != nil { 526 return nil, fmt.Errorf("createEncryptionVM: %w", err) 527 } 528 529 vm, err := did.NewVerificationMethodFromJWK(vmID, vmType, "", j) 530 if err != nil { 531 return nil, fmt.Errorf("createEncryptionVM: %w", err) 532 } 533 534 return vm, nil 535 default: 536 return nil, fmt.Errorf("unsupported verification method for KeyAgreement: '%s'", vmType) 537 } 538 } 539 540 func buildJWKFromBytes(pubKeyBytes []byte) (*jwk.JWK, error) { 541 pubKey := &crypto.PublicKey{} 542 543 err := json.Unmarshal(pubKeyBytes, pubKey) 544 if err != nil { 545 return nil, fmt.Errorf("failed to unmarshal JWK for KeyAgreement: %w", err) 546 } 547 548 var j *jwk.JWK 549 550 switch pubKey.Type { 551 case "EC": 552 ecKey, err := crypto.ToECKey(pubKey) 553 if err != nil { 554 return nil, err 555 } 556 557 j = &jwk.JWK{ 558 JSONWebKey: gojose.JSONWebKey{ 559 Key: ecKey, 560 KeyID: pubKey.KID, 561 }, 562 Kty: pubKey.Type, 563 Crv: pubKey.Curve, 564 } 565 case "OKP": 566 j = &jwk.JWK{ 567 JSONWebKey: gojose.JSONWebKey{ 568 Key: pubKey.X, 569 KeyID: pubKey.KID, 570 }, 571 Kty: pubKey.Type, 572 Crv: pubKey.Curve, 573 } 574 } 575 576 return j, nil 577 } 578 579 func listener( 580 callbacks chan *callback, 581 handleCallbackFunc func(*callback) error) func() { 582 return func() { 583 for c := range callbacks { 584 switch c.msg.Type() { 585 case InvitationMsgType: 586 err := handleCallbackFunc(c) 587 if err != nil { 588 logutil.LogError(logger, Name, "handleCallback", err.Error(), 589 logutil.CreateKeyValueString("msgType", c.msg.Type()), 590 logutil.CreateKeyValueString("msgID", c.msg.ID())) 591 592 continue 593 } 594 default: 595 logutil.LogError(logger, Name, "callbackChannel", "oob/2.0 unsupported msg type", 596 logutil.CreateKeyValueString("msgType", c.msg.Type()), 597 logutil.CreateKeyValueString("msgID", c.msg.ID())) 598 } 599 } 600 } 601 } 602 603 func (s *Service) handleCallback(c *callback) error { 604 switch c.msg.Type() { 605 case InvitationMsgType: 606 return s.handleInvitationCallback(c) 607 default: 608 return fmt.Errorf("unsupported message type: %s", c.msg.Type()) 609 } 610 } 611 612 func (s *Service) handleInvitationCallback(c *callback) error { 613 logger.Debugf("oob/2.0 input: %+v", c) 614 615 err := validateInvitationAcceptance(c.msg, s.myMediaTypeProfiles) 616 if err != nil { 617 return fmt.Errorf("unable to handle invitation: %w", err) 618 } 619 620 return nil 621 } 622 623 func validateInvitationAcceptance(msg service.DIDCommMsg, myProfiles []string) error { 624 if msg.Type() != InvitationMsgType { 625 return nil 626 } 627 628 inv := &Invitation{} 629 630 err := msg.Decode(inv) 631 if err != nil { 632 return fmt.Errorf("validateInvitationAcceptance: failed to decode invitation: %w", err) 633 } 634 635 if !matchMediaTypeProfiles(inv.Body.Accept, myProfiles) { 636 return fmt.Errorf("no acceptable media type profile found in invitation, invitation Accept property: [%v], "+ 637 "agent mediatypeprofiles: [%v]", inv.Body.Accept, myProfiles) 638 } 639 640 return nil 641 } 642 643 func matchMediaTypeProfiles(theirProfiles, myProfiles []string) bool { 644 if theirProfiles == nil { 645 // we use our preferred media type profile instead of confirming an overlap exists 646 return true 647 } 648 649 if myProfiles == nil { 650 myProfiles = transport.MediaTypeProfiles() 651 } 652 653 profiles := list2set(myProfiles) 654 655 for _, a := range theirProfiles { 656 if _, valid := profiles[a]; valid { 657 return true 658 } 659 } 660 661 return false 662 } 663 664 func list2set(list []string) map[string]struct{} { 665 set := map[string]struct{}{} 666 667 for _, e := range list { 668 set[e] = struct{}{} 669 } 670 671 return set 672 }