Skip to content

ldej/go-acapy-client

Repository files navigation

go-acapy-client

A library for interacting with ACA-py in Go.

Context

You can create your own Self-Sovereign Identity solution using the Hyperledger Ursa, Indy, Aries stack. Learn more about the background by watching these videos:

To become an Aries developer, attend these courses by the Linux Foundation on edx.org:

Installation

$ go get -u github.com/ldej/go-acapy-client

Compatibility

Both ACA-py and go-acapy-client are under active development and might be incompatible. Currently go-acapy-client supports v0.6.0-pre of ACA-py.

Development

Start a local Indy ledger network VON-network. Make a checkout of github.com/bcgov/von-network. Then run:

./manage start --logs

This starts 4 Indy nodes and a von-webserver. The von-webserver has a web interface at localhost:9000 which allows you to browse the transactions in the blockchain.

Start a Tails server for the revocation registry tails files: Make a checkout of github.com/bcgov/indy-tails-server. Then run:

./docker/manage start

Start an Aries-Cloud-Agent-Python (ACA-py) instance and configure the right command line parameters. Read about ACA-py and the command line parameters on my blog:

Examples

Create a client, register a DID in the ledger and create an invitation.

package main

import "github.com/ldej/go-acapy-client"

func main() {
    var ledgerURL = "http://localhost:9000"
    var acapyURL = "http://localhost:8000"
    client := acapy.NewClient(acapyURL)
    
    didResponse, err := acapy.RegisterDID(ledgerURL, "Alice", "000000000000000000000000MySeed01", acapy.Endorser)
    if err != nil {
    	// handle error
    }

    // Start aca-py with registered DID
    
    invitation, err := client.CreateInvitation("Bob", false, false, false)
    if err != nil {
        // handle error
    }
}

Examples can be found in the examples folder.

Implemented Endpoints

Action Menu

{id} = connection identifier

Function Name Method Endpoint Implemented
- POST /action-menu/{id}/close
- POST /action-menu/{id}/fetch
- POST /action-menu/{id}/perform
- POST /action-menu/{id}/request
- POST /action-menu/{id}/send-menu

Basic Message

{id} = connection identifier

Function Name Method Endpoint Implemented
SendBasicMessage POST /connections/{id}/send-message ✔️

Connection

{id} = connection identifier

{ref_id} = inbound connection identifier

Function Name Method Endpoint Implemented
QueryConnections GET /connections ✔️
CreateInvitation POST /connections/create-invitation ✔️
- POST /connections/create-static
ReceiveInvitation POST /connections/receive-invitation ✔️
GetConnection GET /connections/{id} ✔️
RemoveConnection DELETE /connections/{id} ✔️
AcceptInvitation POST /connections/{id}/accept-invitation ✔️
AcceptRequest POST /connections/{id}/accept-request ✔️
- POST /connections/{id}/establish-inbound/{ref_id}
- GET /connections/{id}/metadata
- POST /connections/{id}/metadata

Credential Definitions

{id} = credential definition identifier

Function Name Method Endpoint Implemented
CreateCredentialDefinitions POST /credential-definitions ✔️
QueryCredentialDefinitions GET /credential-definitions/created ✔️
GetCredentialDefinition GET /credential-definitions/{id} ✔️

Credentials

{id} = credential identifier, also known as referent

Function Name Method Endpoint Implemented
CredentialMimeTypes GET /credential/mime-types/{id} ✔️
IsCredentialRevoked GET /credential/revoked/{id} ✔️
GetCredential GET /credential/{id} ✔️
RemoveCredential DELETE /credential/{id} ✔️
GetCredentials GET /credentials ✔️

DID Exchange

{id} = connection identifier

Function Name Method Endpoint Implemented
DIDExchangeAcceptInvitation POST /didexchange/{id}/accept-invitation ✔️
DIDExchangeAcceptRequest POST /didexchange/{id}/accept-request ✔️

Introduction

{id} = connection identifier

Function Name Method Endpoint Implemented
- POST /connections/{id}/start-introduction

Issue Credentials (Credential Exchange v1.0)

{id} = credential exchange identifier

Function Name Method Endpoint Implemented
CreateCredentialExchange POST /issue-credential/create ✔️
QueryCredentialExchange GET /issue-credential/records ✔️
GetCredentialExchange GET /issue-credential/records/{id} ✔️
RemoveCredentialExchange DELETE /issue-credential/records/{id} ✔️
IssueCredentialByID POST /issue-credential/records/{id}/issue ✔️
ReportCredentialExchangeProblem POST /issue-credential/records/{id}/problem-report ✔️
SendCredentialOfferByID POST /issue-credential/records/{id}/send-offer ✔️
SendCredentialRequestByID POST /issue-credential/records/{id}/send-request ✔️
StoreReceivedCredential POST /issue-credential/records/{id}/store ✔️
SendCredential POST /issue-credential/send ✔️
SendCredentialOffer POST /issue-credential/send-offer ✔️
SendCredentialProposal POST /issue-credential/send-proposal ✔️

Issue Credentials (Credential Exchange v2.0)

{id} = credential exchange identifier

Function Name Method Endpoint Implemented
CreateCredentialExchangeV2 POST /issue-credential-2.0/create ✔️
QueryCredentialExchangeV2 GET /issue-credential-2.0/records ✔️
GetCredentialExchangeV2 GET /issue-credential-2.0/records/{id} ✔️
RemoveCredentialExchangeV2 DELETE /issue-credential-2.0/records/{id} ✔️
IssueCredentialByIDV2 POST /issue-credential-2.0/records/{id}/issue ✔️
ReportCredentialExchangeProblemV2 POST /issue-credential-2.0/records/{id}/problem-report ✔️
SendCredentialOfferByIDV2 POST /issue-credential-2.0/records/{id}/send-offer ✔️
SendCredentialRequestByIDV2 POST /issue-credential-2.0/records/{id}/send-request ✔️
StoreReceivedCredentialV2 POST /issue-credential-2.0/records/{id}/store ✔️
SendCredentialV2 POST /issue-credential-2.0/send ✔️
SendCredentialOfferV2 POST /issue-credential-2.0/send-offer ✔️
SendCredentialProposalV2 POST /issue-credential-2.0/send-proposal ✔️

Ledger

Function Name Method Endpoint Implemented
GetDIDEndpointFromLedger GET /ledger/did-endpoint ✔️
GetDIDVerkeyFromLedger GET /ledger/did-verkey ✔️
GetDIDRoleFromLedger GET /ledger/get-nym-role ✔️
- POST /ledger/register-nym
- PATCH /ledger/rotate-public-did-keypair
- GET /ledger/taa
- POST /ledger/taa/accept

Mediation

{id} = connection identifier

{mid} = mediation identifier

Function Name Method Endpoint Implemented
- GET /mediation/default-mediator
- DELETE /mediation/default-mediator
- GET /mediation/keylists
- POST /mediation/keylists/{mid}/send-keylist-query
- POST /mediation/keylists{mid}/send-keylist-update
- POST /mediation/request/{id}
- GET /mediation/requests
- GET /mediation/requests/{mid}
DELETE /mediation/requests/{mid}
- POST /mediation/requests/{mid}/deny
- POST /mediation/requests/{mid}/grant
- PUT /mediation/{mid}/default-mediator

Out-of-Band

Function Name Method Endpoint Implemented
CreateOutOfBandInvitation POST /out-of-band/create-invitation ✔️
ReceiveOutOfBandInvitation POST /out-of-band/receive-invitation ✔️

Present Proof

{id} = presentation exchange identifier

Function Name Method Endpoint Implemented
CreatePresentationRequest POST /present-proof/create-request ✔️
QueryPresentationExchange GET /present-proof/records ✔️
GetPresentationExchangeByID GET /present-proof/records/{id} ✔️
RemovePresentationExchangeByID DELETE /present-proof/records/{id} ✔️
GetPresentationCredentialsByID GET /present-proof/records/{id}/credentials ✔️
SendPresentationRequestByID POST /present-proof/records/{id}/send-request ✔️
SendPresentationByID POST /present-proof/records/{id}/send-presentation ✔️
VerifyPresentationByID POST /present-proof/records/{id}/verify-presentation ✔️
SendPresentationProposal POST /present-proof/send-proposal ✔️
SendPresentationRequest POST /present-proof/send-request ✔️

Revocation

{id} = revocation registry identifier, {cred_def_id} = credential definition identifier

Function Name Method Endpoint Implemented
GetActiveRevocationRegistry GET /revocation/active-registry/{cred_def_id} ✔️
ClearPendingRevocations POST /revocation/clear-pending-revocations ✔️
CreateRevocationRegistry POST /revocation/create-registry ✔️
GetCredentialRevocationStatus GET /revocation/credential-record ✔️
PublishRevocations POST /revocation/publish-revocations ✔️
QueryRevocationRegistries GET /revocation/registries/created ✔️
GetRevocationRegistry GET /revocation/registry/{id} ✔️
UpdateRevocationRegistryTailsURI PATCH /revocation/registry/{id} ✔️
PublishRevocationRegistryDefinition POST /revocation/registry/{id}/definition ✔️
PublishRevocationRegistryEntry POST /revocation/registry/{id}/entry ✔️
GetNumberOfIssuedCredentials GET /revocation/registry/{id}/issued ✔️
SetRevocationRegistryState PATCH /revocation/registry/{id}/set-state ✔️
UploadRegistryTailsFile PUT /revocation/registry/{id}/tails-file ✔️
DownloadRegistryTailsFile GET /revocation/registry/{id}/tails-file ✔️
RevokeIssuedCredential POST /revocation/revoke ✔️

Schema

{id} = schema identifier

Function Name Method Endpoint Implemented
RegisterSchema POST /schemas ✔️
QuerySchemas GET /schemas/created ✔️
GetSchema GET /schemas/{id} ✔️

Server

Function Name Method Endpoint Implemented
Features GET /features ✔️
Plugins GET /plugins ✔️
Shutdown GET /shutdown ✔️
Status GET /status ✔️
IsAlive GET /status/live ✔️
IsReady GET /status/ready ✔️
ResetStatistics POST /status/reset ✔️

Trust ping

Function Name Method Endpoint Implemented
SendPing POST /connections/{id}/send-ping ✔️

Wallet

Function Name Method Endpoint Implemented
QueryDIDs GET /wallet/did ✔️
CreateLocalDID POST /wallet/did/create ✔️
RotateKeypair PATCH /wallet/did/local/rotate-keypair ✔️
GetPublicDID GET /wallet/did/public ✔️
SetPublicDID POST /wallet/did/public ✔️
GetDIDEndpointFromWallet GET /wallet/get-public-did ✔️
SetDIDEndpointInWallet POST /wallet/set-public-did ✔️

JSON-LD (unlisted in Swagger)

Function Name Method Endpoint Implemented
SignJSONLD GET /jsonld/sign
VerifyJSONLD GET /jsonld/verify

Webhooks

When an event occurs in ACA-py, for example a connection request has been received, a webhook is called on your controller on a certain topic. go-acapy-client provides a webhook handler where you can register your own functions to handle these events. Based on an event happening you can update your UI or inform the user about the event.

Read more about ACA-py webhooks on my blog.

func main() {
    r := mux.NewRouter()
	webhookHandler := acapy.CreateWebhooksHandler(acapy.WebhookHandlers{
		ConnectionsEventHandler:            ConnectionsEventHandler,
		BasicMessagesEventHandler:          BasicMessagesEventHandler,
		ProblemReportEventHandler:          ProblemReportEventHandler,
		CredentialExchangeEventHandler:     CredentialExchangeEventHandler,
		CredentialExchangeV2EventHandler:   CredentialExchangeV2EventHandler,
		CredentialExchangeDIFEventHandler:  CredentialExchangeDIFEventHandler,
		CredentialExchangeIndyEventHandler: CredentialExchangeIndyEventHandler,
		RevocationRegistryEventHandler:     RevocationRegistryEventHandler,
		PresentationExchangeEventHandler:   PresentationExchangeEventHandler,
		CredentialRevocationEventHandler:   CredentialRevocationEventHandler,
		PingEventHandler:                   PingEventHandler,
		OutOfBandEventHandler:              OutOfBandEventHandler,
	})
    
    r.HandleFunc("/webhooks/topic/{topic}/", webhookHandler).Methods(http.MethodPost)
    
    // and so on
}

func ConnectionsEventHandler(event acapy.ConnectionsEvent) {
    fmt.Printf("\n -> Connection %q (%s), update to state %q rfc23 state %q\n", event.Alias, event.ConnectionID, event.State, event.RFC23State)
}

func BasicMessagesEventHandler(event acapy.BasicMessagesEvent) {
    fmt.Printf("\n -> Received message on connection %s: %s\n", event.ConnectionID, event.Content)
}

func ProblemReportEventHandler(event acapy.ProblemReportEvent) {
    fmt.Printf("\n -> Received problem report: %+v\n", event)
}

func CredentialExchangeEventHandler(event acapy.CredentialExchange) {
    fmt.Printf("\n -> Credential Exchange update: %s - %s\n", event.CredentialExchangeID, event.State)
}

func CredentialExchangeV2EventHandler(event acapy.CredentialExchangeRecordV2) {
	fmt.Printf("\n -> Credential Exchange V2 update: %s - %s\n", event.CredentialExchangeID, event.State)
}

func CredentialExchangeDIFEventHandler(event acapy.CredentialExchangeDIF) {
	fmt.Printf("\n -> Credential Exchange DIF Event: %s - %s", event.CredentialExchangeID, event.State)
}

func CredentialExchangeIndyEventHandler(event acapy.CredentialExchangeIndy) {
	fmt.Printf("\n -> Credential Exchange Indy Event: %s - %s", event.CredentialExchangeID, event.CredentialExchangeIndyID)
}

func RevocationRegistryEventHandler(event acapy.RevocationRegistry) {
    fmt.Printf("\n -> Revocation Registry update: %s - %s\n", event.RevocationRegistryID, event.State)
}

func PresentationExchangeEventHandler(event acapy.PresentationExchange) {
    fmt.Printf("\n -> Presentation Exchange update: %s - %s\n", event.PresentationExchangeID, event.State)
}

func CredentialRevocationEventHandler(event acapy.IssuerCredentialRevocationEvent) {
    fmt.Printf("\n -> Issuer Credential Revocation: %s - %s - %s\n", event.CredentialExchangeID, event.RecordID, event.State)
}

func PingEventHandler(event acapy.PingEvent) {
    fmt.Printf("\n -> Ping Event: %q state: %q responded: %t\n", event.ConnectionID, event.State, event.Responded)
}

func OutOfBandEventHandler(event acapy.OutOfBandEvent) {
    fmt.Printf("\n -> Out of Band Event: %q state %q\n", event.InvitationID, event.State)
}

You are free to choose the URL for your webhooks. Don't forget to set the command-line parameter for ACA-py: --webhook-url http://localhost:{port}/webhooks. The URL you provide to ACA-py is the base URL which will be extended with /topic/{topic} by default. So whatever URL you choose, make sure that:

  • if the --webhook-url is http://myhost:{port}/webhooks
  • then the webhooks handler should listen on http://myhost:{port}/webhooks/topic/{topic}

The acapy.WebhookHandler is web framework agnostic and reads the topic from the URL by itself. The handler returned by acapy.WebhookHandler has the standard handler signature func (w http.ResponseWriter, r *http.Request) {}.

TODO

  • godoc
  • Proper error handling
  • Admin API Key
  • Tracing via global config
  • Automation of steps via global config
  • Payment decorators https://github.com/hyperledger/aries-rfcs/tree/master/features/0075-payment-decorators
  • Constructors for JSON-LD types
  • Add types for roles, states, predicates
  • Allow for a connection-less credential exchange
  • Allow for a connection-less proof by making a QR code of a payload below. The base64 payload is the result of your call to /present-proof/create-request.
{
    "@id": "3b67c4bf-3953-4ace-94ef-28e0969288c5",
    "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/request-presentation",
    "request_presentations~attach": [
        {
            "@id": "libindy-request-presentation-0",
            "mime-type": "application/json",
            "data": {
                "base64": "eyJuYW1lIjoiQ29udGFjdCBBZGRyZXNzIiwidmVyc2lvbiI6IjEuMC4wIiwibm9uY2UiOiIzOTIyNTEwMjk1NjY5MzcxNDIxNTIzMDgiLCJyZXF1ZXN0ZWRfYXR0cmlidXRlcyI6eyJHaXZlbiBOYW1lcyI6eyJuYW1lIjoiZ2l2ZW5fbmFtZXMiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIkZhbWlseSBOYW1lIjp7Im5hbWUiOiJmYW1pbHlfbmFtZSIsInJlc3RyaWN0aW9ucyI6W3sic2NoZW1hX2lzc3Vlcl9kaWQiOiI4NTQ1OUd4ak55U0o4SHdUVFE0dnE3Iiwic2NoZW1hX25hbWUiOiJ2ZXJpZmllZF9wZXJzb24iLCJzY2hlbWFfdmVyc2lvbiI6IjEuNC4wIn0seyJzY2hlbWFfbmFtZSI6InVudmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIwLjEuMCIsImlzc3Vlcl9kaWQiOiI4WXE3RWhLQk11amgyNU5rTEdHYjJ0In1dfSwiRGF0ZSBvZiBCaXJ0aCI6eyJuYW1lIjoiYmlydGhkYXRlIiwicmVzdHJpY3Rpb25zIjpbeyJzY2hlbWFfaXNzdWVyX2RpZCI6Ijg1NDU5R3hqTnlTSjhId1RUUTR2cTciLCJzY2hlbWFfbmFtZSI6InZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMS40LjAifSx7InNjaGVtYV9uYW1lIjoidW52ZXJpZmllZF9wZXJzb24iLCJzY2hlbWFfdmVyc2lvbiI6IjAuMS4wIiwiaXNzdWVyX2RpZCI6IjhZcTdFaEtCTXVqaDI1TmtMR0diMnQifV19LCJTdHJlZXQgQWRkcmVzcyI6eyJuYW1lIjoic3RyZWV0X2FkZHJlc3MiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIlBvc3RhbCBDb2RlIjp7Im5hbWUiOiJwb3N0YWxfY29kZSIsInJlc3RyaWN0aW9ucyI6W3sic2NoZW1hX2lzc3Vlcl9kaWQiOiI4NTQ1OUd4ak55U0o4SHdUVFE0dnE3Iiwic2NoZW1hX25hbWUiOiJ2ZXJpZmllZF9wZXJzb24iLCJzY2hlbWFfdmVyc2lvbiI6IjEuNC4wIn0seyJzY2hlbWFfbmFtZSI6InVudmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIwLjEuMCIsImlzc3Vlcl9kaWQiOiI4WXE3RWhLQk11amgyNU5rTEdHYjJ0In1dfSwiQ2l0eSI6eyJuYW1lIjoibG9jYWxpdHkiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIlByb3ZpbmNlIjp7Im5hbWUiOiJyZWdpb24iLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIkNvdW50cnkiOnsibmFtZSI6ImNvdW50cnkiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319"
            }
        }
    ],
    "comment": null,
    "~service": {
        "recipientKeys": [
            "F2fFPEXABoPKt8mYjNAavBwbsmQKYqNTcv3HKqBgqpLw"
        ],
        "routingKeys": null,
        "serviceEndpoint": "https://my-url.test.org"
    }
}