github.com/hyperledger/aries-framework-go@v0.3.2/pkg/client/outofband/client.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package outofband 8 9 import ( 10 "errors" 11 "fmt" 12 13 "github.com/google/uuid" 14 15 "github.com/hyperledger/aries-framework-go/pkg/common/model" 16 "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" 17 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" 18 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" 19 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator" 20 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" 21 "github.com/hyperledger/aries-framework-go/pkg/didcomm/transport" 22 "github.com/hyperledger/aries-framework-go/pkg/doc/did" 23 "github.com/hyperledger/aries-framework-go/pkg/doc/util/kmsdidkey" 24 "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" 25 "github.com/hyperledger/aries-framework-go/pkg/kms" 26 ) 27 28 type ( 29 // Invitation is this protocol's `invitation` message. 30 Invitation outofband.Invitation 31 // Action contains helpful information about action. 32 Action outofband.Action 33 ) 34 35 const ( 36 // InvitationMsgType is the '@type' for the invitation message. 37 InvitationMsgType = outofband.InvitationMsgType 38 // HandshakeReuseMsgType is the '@type' for the handshake reuse message. 39 HandshakeReuseMsgType = outofband.HandshakeReuseMsgType 40 // HandshakeReuseAcceptedMsgType is the '@type' for the handshake reuse accepted message. 41 HandshakeReuseAcceptedMsgType = outofband.HandshakeReuseAcceptedMsgType 42 ) 43 44 // EventOptions are is a container of options that you can pass to an event's 45 // Continue function to customize the reaction to incoming out-of-band messages. 46 type EventOptions struct { 47 // Label will be shared with the other agent during the subsequent did-exchange. 48 Label string 49 // Connections allows specifying router connections. 50 Connections []string 51 ReuseAny bool 52 ReuseDID string 53 } 54 55 // RouterConnections return router connections. 56 func (e *EventOptions) RouterConnections() []string { 57 return e.Connections 58 } 59 60 // MyLabel will be shared with the other agent during the subsequent did-exchange. 61 func (e *EventOptions) MyLabel() string { 62 return e.Label 63 } 64 65 // ReuseAnyConnection signals whether to use any recognized DID in the services array for a reusable connection. 66 func (e *EventOptions) ReuseAnyConnection() bool { 67 return e.ReuseAny 68 } 69 70 // ReuseConnection returns the DID to be used when reusing a connection. 71 func (e *EventOptions) ReuseConnection() string { 72 return e.ReuseDID 73 } 74 75 // Event is a container of out-of-band protocol-specific properties for DIDCommActions and StateMsgs. 76 type Event interface { 77 // ConnectionID of the connection record, once it's created. 78 // This becomes available in a post-state event unless an error condition is encountered. 79 ConnectionID() string 80 // Error is non-nil if an error is encountered. 81 Error() error 82 } 83 84 // MessageOption allow you to customize the way out-of-band messages are built. 85 type MessageOption func(*message) 86 87 type message struct { 88 Label string 89 Goal string 90 GoalCode string 91 RouterConnections []string 92 Service []interface{} 93 HandshakeProtocols []string 94 Attachments []*decorator.Attachment 95 Accept []string 96 ReuseAnyConnection bool 97 ReuseConnection string 98 } 99 100 func (m *message) RouterConnection() string { 101 if len(m.RouterConnections) == 0 { 102 return "" 103 } 104 105 return m.RouterConnections[0] 106 } 107 108 // OobService defines the outofband service. 109 type OobService interface { 110 service.Event 111 AcceptInvitation(*outofband.Invitation, outofband.Options) (string, error) 112 SaveInvitation(*outofband.Invitation) error 113 Actions() ([]outofband.Action, error) 114 ActionContinue(string, outofband.Options) error 115 ActionStop(string, error) error 116 } 117 118 // Provider provides the dependencies for the client. 119 type Provider interface { 120 ServiceEndpoint() string 121 Service(id string) (interface{}, error) 122 KMS() kms.KeyManager 123 KeyType() kms.KeyType 124 KeyAgreementType() kms.KeyType 125 MediaTypeProfiles() []string 126 } 127 128 // Client for the Out-Of-Band protocol: 129 // https://github.com/hyperledger/aries-rfcs/blob/master/features/0434-outofband/README.md 130 type Client struct { 131 service.Event 132 didDocSvcFunc func(routerConnID string, accept []string) (*did.Service, error) 133 oobService OobService 134 mediaTypeProfiles []string 135 } 136 137 // New returns a new Client for the Out-Of-Band protocol. 138 func New(p Provider) (*Client, error) { 139 s, err := p.Service(outofband.Name) 140 if err != nil { 141 return nil, fmt.Errorf("failed to look up service %s : %w", outofband.Name, err) 142 } 143 144 oobSvc, ok := s.(OobService) 145 if !ok { 146 return nil, fmt.Errorf("failed to cast service %s as a dependency", outofband.Name) 147 } 148 149 mtp := p.MediaTypeProfiles() 150 151 if len(mtp) == 0 { 152 mtp = []string{transport.MediaTypeAIP2RFC0019Profile} 153 } 154 155 client := &Client{ 156 Event: oobSvc, 157 oobService: oobSvc, 158 mediaTypeProfiles: mtp, 159 } 160 161 client.didDocSvcFunc = client.didServiceBlockFunc(p) 162 163 return client, nil 164 } 165 166 // CreateInvitation creates and saves an out-of-band invitation. 167 // Services are required in the RFC, but optional in this implementation. If not provided, a default will be assigned. 168 // TODO HandShakeProtocols are optional in the RFC and as arguments to this function. 169 // However, if not provided, a default will be assigned for you. 170 func (c *Client) CreateInvitation(services []interface{}, opts ...MessageOption) (*Invitation, error) { 171 msg := &message{} 172 173 for _, opt := range opts { 174 opt(msg) 175 } 176 177 inv := &Invitation{ 178 ID: uuid.New().String(), 179 Type: InvitationMsgType, 180 Label: msg.Label, 181 Goal: msg.Goal, 182 GoalCode: msg.GoalCode, 183 Services: services, 184 Accept: msg.Accept, 185 Protocols: msg.HandshakeProtocols, 186 Requests: msg.Attachments, 187 } 188 189 if len(inv.Accept) == 0 { 190 inv.Accept = c.mediaTypeProfiles 191 } 192 193 if len(inv.Services) == 0 { 194 svc, err := c.didDocSvcFunc(msg.RouterConnection(), inv.Accept) 195 if err != nil { 196 return nil, fmt.Errorf("failed to create a new inlined did doc service block : %w", err) 197 } 198 199 inv.Services = []interface{}{svc} 200 } else { 201 err := validateServices(inv.Services...) 202 if err != nil { 203 return nil, fmt.Errorf("invalid service: %w", err) 204 } 205 } 206 207 if len(inv.Protocols) == 0 { 208 // TODO should be injected into client 209 // https://github.com/hyperledger/aries-framework-go/issues/1691 210 inv.Protocols = []string{didexchange.PIURI} 211 } 212 213 cast := outofband.Invitation(*inv) 214 215 err := c.oobService.SaveInvitation(&cast) 216 if err != nil { 217 return nil, fmt.Errorf("failed to save outofband invitation : %w", err) 218 } 219 220 return inv, nil 221 } 222 223 // Actions returns unfinished actions for the async usage. 224 func (c *Client) Actions() ([]Action, error) { 225 actions, err := c.oobService.Actions() 226 if err != nil { 227 return nil, err 228 } 229 230 result := make([]Action, len(actions)) 231 for i, action := range actions { 232 result[i] = Action(action) 233 } 234 235 return result, nil 236 } 237 238 // ActionContinue allows continuing with the protocol after an action event was triggered. 239 func (c *Client) ActionContinue(piID, label string, opts ...MessageOption) error { 240 msg := &message{} 241 242 for _, opt := range opts { 243 opt(msg) 244 } 245 246 return c.oobService.ActionContinue(piID, &EventOptions{ 247 Label: label, 248 Connections: msg.RouterConnections, 249 ReuseAny: msg.ReuseAnyConnection, 250 ReuseDID: msg.ReuseConnection, 251 }) 252 } 253 254 // ActionStop stops the protocol after an action event was triggered. 255 func (c *Client) ActionStop(piID string, err error) error { 256 return c.oobService.ActionStop(piID, err) 257 } 258 259 // AcceptInvitation from another agent and return the ID of the new connection records. 260 func (c *Client) AcceptInvitation(i *Invitation, myLabel string, opts ...MessageOption) (string, error) { 261 msg := &message{} 262 263 for _, opt := range opts { 264 opt(msg) 265 } 266 267 cast := outofband.Invitation(*i) 268 269 connID, err := c.oobService.AcceptInvitation( 270 &cast, 271 &EventOptions{ 272 Label: myLabel, 273 ReuseAny: msg.ReuseAnyConnection, 274 ReuseDID: msg.ReuseConnection, 275 Connections: msg.RouterConnections, 276 }, 277 ) 278 if err != nil { 279 return "", fmt.Errorf("out-of-band service failed to accept invitation : %w", err) 280 } 281 282 return connID, err 283 } 284 285 // WithLabel allows you to specify the label on the message. 286 func WithLabel(l string) MessageOption { 287 return func(m *message) { 288 m.Label = l 289 } 290 } 291 292 // WithGoal allows you to specify the `goal` and `goalCode` for the message. 293 func WithGoal(goal, goalCode string) MessageOption { 294 return func(m *message) { 295 m.Goal = goal 296 m.GoalCode = goalCode 297 } 298 } 299 300 // WithRouterConnections allows you to specify the router connections. 301 func WithRouterConnections(conn ...string) MessageOption { 302 return func(m *message) { 303 for _, c := range conn { 304 // filters out empty connections 305 if c != "" { 306 m.RouterConnections = append(m.RouterConnections, c) 307 } 308 } 309 } 310 } 311 312 // WithHandshakeProtocols allows you to customize the handshake_protocols to include in the Invitation. 313 func WithHandshakeProtocols(proto ...string) MessageOption { 314 return func(m *message) { 315 m.HandshakeProtocols = proto 316 } 317 } 318 319 // WithAttachments allows you to include attachments in the Invitation. 320 func WithAttachments(a ...*decorator.Attachment) MessageOption { 321 return func(m *message) { 322 m.Attachments = a 323 } 324 } 325 326 // WithAccept will set the given media type profiles in the Invitation's `accept` property. 327 // Only valid values from RFC 0044 are supported. 328 func WithAccept(a ...string) MessageOption { 329 return func(m *message) { 330 m.Accept = a 331 } 332 } 333 334 // ReuseAnyConnection is used when accepting an invitation with either AcceptInvitation or ActionContinue. 335 // The `services` array will be scanned until it finds a recognized DID entry and send a `handshake-reuse` message 336 // to its did-communication service endpoint. 337 // Cannot be used together with ReuseConnection. 338 func ReuseAnyConnection() MessageOption { 339 return func(m *message) { 340 m.ReuseAnyConnection = true 341 } 342 } 343 344 // ReuseConnection is used when accepting an invitation with either AcceptInvitation or ActionContinue. 345 // 'did' must be an entry in the Invitation's 'services' array. A `handshake-reuse` message is sent to its 346 // did-communication service endpoint. 347 // Cannot be used together with ReuseAnyConnection. 348 func ReuseConnection(theirDID string) MessageOption { 349 return func(m *message) { 350 m.ReuseConnection = theirDID 351 } 352 } 353 354 func validateServices(svcs ...interface{}) error { 355 for i := range svcs { 356 switch svc := svcs[i].(type) { 357 case string: 358 _, err := did.Parse(svc) 359 if err != nil { 360 return fmt.Errorf("invalid DID [%s]: %w", svc, err) 361 } 362 case did.Service, *did.Service: 363 default: 364 return fmt.Errorf("unsupported service data type: %+v", svc) 365 } 366 } 367 368 return nil 369 } 370 371 // DidDocServiceFunc returns a function that returns a DID doc `service` entry. 372 // Used when no service entries are specified when creating messages. 373 //nolint:funlen,gocyclo 374 func (c *Client) didServiceBlockFunc(p Provider) func(routerConnID string, accept []string) (*did.Service, error) { 375 return func(routerConnID string, accept []string) (*did.Service, error) { 376 var ( 377 keyType kms.KeyType 378 didCommServiceType string 379 sp model.Endpoint 380 ) 381 382 useDIDCommV2 := isDIDCommV2(accept) 383 // TODO https://github.com/hyperledger/aries-framework-go/issues/623 'alias' should be passed as arg and persisted 384 // with connection record 385 if useDIDCommV2 { 386 keyType = p.KeyAgreementType() 387 didCommServiceType = vdr.DIDCommV2ServiceType 388 sp = model.NewDIDCommV2Endpoint([]model.DIDCommV2Endpoint{{ 389 URI: p.ServiceEndpoint(), 390 Accept: p.MediaTypeProfiles(), 391 }}) 392 } else { 393 keyType = p.KeyType() 394 didCommServiceType = vdr.DIDCommServiceType 395 sp = model.NewDIDCommV1Endpoint(p.ServiceEndpoint()) 396 } 397 398 if string(keyType) == "" { 399 keyType = kms.ED25519Type 400 } 401 402 _, pubKey, err := p.KMS().CreateAndExportPubKeyBytes(keyType) 403 if err != nil { 404 return nil, fmt.Errorf("didServiceBlockFunc: failed to create and extract public SigningKey bytes: %w", err) 405 } 406 407 s, err := p.Service(mediator.Coordination) 408 if err != nil { 409 return nil, fmt.Errorf("didServiceBlockFunc: failed Coordinate Mediate service: %w", err) 410 } 411 412 routeSvc, ok := s.(mediator.ProtocolService) 413 if !ok { 414 return nil, errors.New("didServiceBlockFunc: cast service to Route Service failed") 415 } 416 417 didKey, err := kmsdidkey.BuildDIDKeyByKeyType(pubKey, keyType) 418 if err != nil { 419 return nil, fmt.Errorf("didServiceBlockFunc: failed to build did:key for key type '%v': %w", keyType, err) 420 } 421 422 if routerConnID == "" { 423 return &did.Service{ 424 ID: uuid.New().String(), 425 Type: didCommServiceType, 426 RecipientKeys: []string{didKey}, 427 ServiceEndpoint: sp, 428 }, nil 429 } 430 431 // get the route configs 432 serviceEndpoint, routingKeys, err := mediator.GetRouterConfig(routeSvc, routerConnID, p.ServiceEndpoint()) 433 if err != nil { 434 return nil, fmt.Errorf("didServiceBlockFunc: create invitation - fetch router config : %w", err) 435 } 436 437 if err = mediator.AddKeyToRouter(routeSvc, routerConnID, didKey); err != nil { 438 return nil, fmt.Errorf("didServiceBlockFunc: create invitation - failed to add key to the router : %w", err) 439 } 440 441 var svc *did.Service 442 443 if useDIDCommV2 { 444 sp = model.NewDIDCommV2Endpoint([]model.DIDCommV2Endpoint{{ 445 URI: serviceEndpoint, 446 Accept: accept, 447 RoutingKeys: routingKeys, 448 }}) 449 svc = &did.Service{ 450 ID: uuid.New().String(), 451 Type: didCommServiceType, 452 RecipientKeys: []string{didKey}, 453 ServiceEndpoint: sp, 454 } 455 } else { 456 sp = model.NewDIDCommV1Endpoint(serviceEndpoint) 457 svc = &did.Service{ 458 ID: uuid.New().String(), 459 Type: didCommServiceType, 460 RecipientKeys: []string{didKey}, 461 ServiceEndpoint: sp, 462 RoutingKeys: routingKeys, 463 Accept: accept, 464 } 465 } 466 467 return svc, nil 468 } 469 } 470 471 func isDIDCommV2(accept []string) bool { 472 for _, a := range accept { 473 switch a { 474 case transport.MediaTypeDIDCommV2Profile, transport.MediaTypeAIP2RFC0587Profile, 475 transport.MediaTypeV2EncryptedEnvelope, transport.MediaTypeV2EncryptedEnvelopeV1PlaintextPayload, 476 transport.MediaTypeV1EncryptedEnvelope: 477 return true 478 } 479 } 480 481 return false 482 }