package main
import (
"context"
"errors"
"fmt"
"path"
"github.com/algorand/go-algorand-sdk/crypto"
"github.com/algorand/go-algorand-sdk/types"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// strategy for creating multi-sign algorand addresses. The user
// addresses and operator addresses, and one vault-created address are
// combined to make the total signer list. The threshold must equal
// the number of user keys, plus one (the vault key). The number of
// operator keys must meet or exceed the threshold. These rules imply
// the user needs one signature provided by vault to complete a
// transaction, while operator can complete transactions without
// vault, should anything go seriously wrong with vault configuration.
type multisig struct {
Address string
// TODO(dnc): are the details from Algorand SDK struct needed here?
Multisig crypto.MultisigAccount
// User configured signing addresses
UserSigner []Signer
// signing key created by this plugin
VaultSigner []Signer // today, always one key, but use slice here in case that changes
// signing keys configured by operator. Redundant with config data, but here we keep ordered list.
OperatorSigner []Signer
}
// Read address configuration from Vault storage. If not yet
// configured, nil is returned.
func (b *backend) address(ctx context.Context, store logical.Storage, name string) (*multisig, error) {
entry, err := store.Get(ctx, "address/"+name)
if err != nil {
return nil, err
}
if entry == nil { // path has not been updated
return nil, nil
}
addr := &multisig{}
err = entry.DecodeJSON(&addr)
return addr, err
}
func (b *backend) pathAddress() *framework.Path {
return &framework.Path{
Pattern: "address/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the signer",
},
"signer": &framework.FieldSchema{
Type: framework.TypeStringSlice,
Description: "User public key(s), in Algorand address encoding",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathAddressUpdate,
logical.ReadOperation: b.pathAddressRead,
// TODO(dnc): delete and list
},
HelpSynopsis: "Create an Algorang wallet, with specified user signing key(s)",
HelpDescription: `
In multi-sign configuration, produces an Algorand address requiring
signatures from each of the specified user keys, and also a key known
only to Vault.
The address will also allow signing by operator keys, which serve as a
fallback in the unlikely event that the user or vault keys are lost.
`}
}
func (b *backend) pathAddressRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
addr, err := b.address(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if addr == nil {
return nil, fmt.Errorf("address (%q) not found", name)
}
res := &logical.Response{Data: map[string]interface{}{
"name": name,
"signer": addr.UserSigner,
"address": addr.Address,
}}
return res, nil
}
func (b *backend) pathAddressUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.config(ctx, req.Storage)
if err != nil {
return nil, err
}
if cfg.Threshold == 0 {
// TODO(dnc): support single-sign, using key known only to vault
return nil, fmt.Errorf("single-sign Algorand address is not yet supported; write %s to configure multi-sign", path.Join(req.MountPoint, "config"))
}
name := d.Get("name").(string)
addr, err := b.address(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if addr != nil {
return nil, errors.New("update Algorand address is not supported; consider a new address instead")
}
multi := &multisig{} // create new multisig address
signer, ok := d.GetOk("signer")
if ok {
multi.UserSigner = make([]Signer, 0, len(signer.([]string)))
for _, a := range signer.([]string) {
address, err := types.DecodeAddress(a)
if err != nil {
return nil, fmt.Errorf("bad Algorand address (%q): %w", a, err)
}
multi.UserSigner = append(multi.UserSigner, Signer(address))
}
}
// Threshold should be exactly the number of user signers plus one Vault signer.
if len(multi.UserSigner) != int(cfg.Threshold)-1 {
return nil, fmt.Errorf("multi-sign threshold (%d) requires %d user signing addresses (got %d)", cfg.Threshold, cfg.Threshold-1, len(multi.UserSigner))
}
// create a key for this plugin to sign for this address
keypair := crypto.GenerateAccount()
entry, err := logical.StorageEntryJSON("keypair/"+name, keypair) // TODO(dnc): storing data structure from Algorand SDK, should we avoid struct that might change out from under us?
if err != nil {
return nil, fmt.Errorf("failed to encode keypair: %w", err)
}
err = req.Storage.Put(ctx, entry)
if err != nil {
return nil, err
}
multi.VaultSigner = []Signer{Signer(keypair.Address)}
// copy from cfg.Signer in random order, place in
// multi.OperatorSigner in a specific order. Which address comes
// first does not matter in terms of privilege, but it does affect
// the address of the resulting multisign wallet (same set of
// addresses, in different order, produces a different address), so
// we remember the order we used in multi.OperatorSigner.
multi.OperatorSigner = make([]Signer, 0, len(cfg.Signer))
for _, a := range cfg.Signer {
multi.OperatorSigner = append(multi.OperatorSigner, a)
}
// Gather all keys in multisign scheme
order := make([]types.Address, 0, len(multi.OperatorSigner)+len(multi.UserSigner)+len(multi.VaultSigner))
unique := make(map[Signer]struct{})
var dummy struct{}
for _, signer := range [][]Signer{multi.VaultSigner, multi.UserSigner, multi.OperatorSigner} {
// loop to cast our Signer as types.Address for call to MultisigAccountWithParams
for _, s := range signer {
order = append(order, types.Address(s))
_, ok := unique[s]
if ok {
return nil, fmt.Errorf("address (%s) appears twice in signer list", s)
}
unique[s] = dummy
}
}
// Build an Algorand multisig address
multi.Multisig, err = crypto.MultisigAccountWithParams(1 /* version */, cfg.Threshold, order)
if err != nil {
return nil, fmt.Errorf("failed to build Algorand multisig: %w", err)
}
multiAddress, err := multi.Multisig.Address()
if err != nil {
return nil, err
}
multi.Address = multiAddress.String()
// Store address
entry, err = logical.StorageEntryJSON("address/"+name, multi)
if err != nil {
return nil, fmt.Errorf("failed to encode multisig address: %w", err)
}
err = req.Storage.Put(ctx, entry)
if err != nil {
return nil, err
}
res := &logical.Response{Data: map[string]interface{}{
"name": name,
"signer": multi.UserSigner,
"address": multi.Address,
}}
return res, nil
}