commit 15a1fecc1a177c79bfe7dcfa462810f7d09fad1b
parent dfdf9bbb7c928827928f3855ff5afea91daa28b9
Author: Joel-Haeberli <haebu@rubigen.ch>
Date: Fri, 5 Apr 2024 23:23:36 +0200
code: cli for managing providers and terminals
Diffstat:
25 files changed, 920 insertions(+), 20 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -3,6 +3,7 @@ schemaspy/Makefile
infra/
bruno/
c2ec/c2ec
+cli/c2ec-cli
LocalMakefile
diff --git a/bruno/c2ec/(LOCAL-BIA) Payment Confirmation.bru b/bruno/c2ec/(LOCAL-BIA) Payment Confirmation.bru
@@ -5,7 +5,23 @@ meta {
}
post {
- url: http://localhost:8081/c2ec/withdrawal-operation/WOPID
- body: none
+ url: http://localhost:8081/c2ec/withdrawal-operation/WOPID/payment
+ body: json
auth: none
}
+
+body:json {
+ {
+ "provider_transaction_id": "",
+ "amount": {
+ "currency": "CHF",
+ "value": "",
+ "fraction": ""
+ },
+ "fees": {
+ "currency": "CHF",
+ "value": "",
+ "fraction": ""
+ }
+ }
+}
diff --git a/bruno/c2ec/(LOCAL-BIA) Register Withdrawal.bru b/bruno/c2ec/(LOCAL-BIA) Register Withdrawal.bru
@@ -5,7 +5,21 @@ meta {
}
post {
- url: http://localhost:8081/c2ec/withdrawal-operation
- body: none
+ url: http://localhost:8081/c2ec/withdrawal-operation/
+ body: json
auth: none
}
+
+body:json {
+ {
+ "status": "pending",
+ "amount": {
+ "currency": "CHF",
+ "value": "",
+ "fraction": ""
+ },
+ "sender_wire": "payto://wallee-transaction/asdfsadf",
+ "wire_types": ["wallee-transaction"],
+ "reserve_public_key": ""
+ }
+}
diff --git a/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru b/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru
@@ -5,7 +5,12 @@ meta {
}
get {
- url: http://localhost:8081/c2ec/withdrawal-operation/
+ url: http://localhost:8081/c2ec/withdrawal-operation/WOPID?long_poll_ms=5000&old_state=pending
body: none
auth: none
}
+
+query {
+ long_poll_ms: 5000
+ old_state: pending
+}
diff --git a/c2ec/auth.go b/c2ec/auth.go
@@ -1,7 +1,12 @@
package main
import (
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
"net/http"
+
+ "golang.org/x/crypto/argon2"
)
const AUTHORIZATION_HEADER = "Authorization"
@@ -54,3 +59,23 @@ func isAllowed(req *http.Request) bool {
// a wopid and
// func handleTokenRequest() {
// }
+
+func pbkdf(pw string) (string, error) {
+
+ rfcTime := 3
+ rfcMemory := 32 * 1024
+ salt := make([]byte, 16)
+ _, err := rand.Read(salt)
+ if err != nil {
+ return "", err
+ }
+ key := argon2.Key([]byte(pw), salt, uint32(rfcTime), uint32(rfcMemory), 4, 32)
+
+ keyAndSalt := make([]byte, 0, 48)
+ keyAndSalt = append(keyAndSalt, key...)
+ keyAndSalt = append(keyAndSalt, salt...)
+ if len(keyAndSalt) != 48 {
+ return "", errors.New("invalid password hash and salt")
+ }
+ return base64.StdEncoding.EncodeToString(keyAndSalt), nil
+}
diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go
@@ -99,8 +99,7 @@ func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) {
// read and validate the wopid path parameter
wopid := req.PathValue(WOPID_PARAMETER)
- if _, ok := any(wopid).(WithdrawalIdentifier); !ok {
-
+ if !WopidValid(wopid) {
if wopid == "" {
err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_PATH_PARAMETER",
@@ -168,7 +167,7 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
// read and validate the wopid path parameter
wopid := req.PathValue(WOPID_PARAMETER)
- if _, ok := any(wopid).(WithdrawalIdentifier); !ok {
+ if !WopidValid(wopid) {
if wopid == "" {
err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
@@ -234,7 +233,7 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
func handlePaymentNotification(res http.ResponseWriter, req *http.Request) {
wopid := req.PathValue(WOPID_PARAMETER)
- if _, ok := any(wopid).(WithdrawalIdentifier); !ok {
+ if !WopidValid(wopid) {
if wopid == "" {
err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
diff --git a/c2ec/base32-taler.go b/c2ec/base32-taler.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "errors"
+ "strings"
+ "unicode"
+)
+
+// just ported the function from the C code in Taler.
+// source: [taler-exchange]/src/util/crypto_confirmation.c
+func TalerBase32Decode(val string, buf []byte) (int, error) {
+ // 32 characters for decoding, using RFC 3548.
+ decTable := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+ udata := buf
+ wpos := 0
+ rpos := 0
+ bits := 0
+ vbit := 0
+
+ for rpos < len(val) || vbit >= 8 {
+ if rpos < len(val) && vbit < 8 {
+ c := val[rpos]
+
+ if c == '=' {
+ // padding character
+ if rpos == len(val)-1 {
+ break // Ok, 1x '=' padding is allowed
+ }
+ if c == '=' && rpos+1 == len(val)-1 {
+ break // Ok, 2x '=' padding is allowed
+ }
+ return -1, errors.New("invalid padding")
+ }
+ p := strings.IndexByte(strings.ToUpper(decTable), byte(unicode.ToUpper(rune(c))))
+ if p == -1 {
+ return -1, errors.New("invalid character")
+ }
+ bits = (bits << 5) | p
+ vbit += 5
+ rpos++
+ }
+ if vbit >= 8 {
+ udata[wpos] = byte((bits >> (vbit - 8)) & 0xFF)
+ wpos++
+ vbit -= 8
+ }
+ }
+ return wpos, nil
+}
diff --git a/c2ec/base32-taler_test.go b/c2ec/base32-taler_test.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestBase32(t *testing.T) {
+
+ val := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ key := make([]byte, len(val))
+ result, err := TalerBase32Decode(val, key)
+ if err != nil {
+ fmt.Println(err.Error())
+ t.FailNow()
+ }
+
+ fmt.Println(key)
+
+ fmt.Println(result)
+}
diff --git a/c2ec/c2ec-config.yaml b/c2ec/c2ec-config.yaml
@@ -13,5 +13,7 @@ db:
password: "local"
database: "postgres"
providers:
- - "Wallee"
- - "Simulation"
+ - name: "Wallee"
+ credentials-password: "secret"
+ - name: "Simulation"
+ credentials-password: "secret"
diff --git a/c2ec/config.go b/c2ec/config.go
@@ -7,9 +7,9 @@ import (
)
type C2ECConfig struct {
- Server C2ECServerConfig `yaml:"c2ec"`
- Database C2ECDatabseConfig `yaml:"db"`
- Providers []string `yaml:"providers"`
+ Server C2ECServerConfig `yaml:"c2ec"`
+ Database C2ECDatabseConfig `yaml:"db"`
+ Providers []C2ECProviderConfig `yaml:"providers"`
}
type C2ECServerConfig struct {
@@ -30,6 +30,11 @@ type C2ECDatabseConfig struct {
Database string `yaml:"database"`
}
+type C2ECProviderConfig struct {
+ Name string `yaml:"name"`
+ CredentialsPassword string `yaml:"credentials-password"`
+}
+
func Parse(path string) (*C2ECConfig, error) {
f, err := os.Open(path)
diff --git a/c2ec/go.mod b/c2ec/go.mod
@@ -10,12 +10,14 @@ require (
)
require (
+ github.com/carlmjohnson/crockford v0.23.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
- golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/crypto v0.22.0 // indirect
golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
diff --git a/c2ec/go.sum b/c2ec/go.sum
@@ -1,3 +1,5 @@
+github.com/carlmjohnson/crockford v0.23.1 h1:ImVIp5KOZvHXpxaVGP78WanEPxzH0ZqqE4Nd9YuJAb4=
+github.com/carlmjohnson/crockford v0.23.1/go.mod h1:+uz/aAJerF/noKb/fS4l9vl4bFexgpDjdVoL9XeKn/0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -31,8 +33,12 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/c2ec/main.go b/c2ec/main.go
@@ -107,18 +107,18 @@ func setupAttestors(cfg *C2ECConfig) error {
return errors.New("setup database first")
}
- for _, providerName := range cfg.Providers {
+ for _, provider := range cfg.Providers {
- p, err := DB.GetTerminalProviderByName(providerName)
+ p, err := DB.GetTerminalProviderByName(provider.Name)
if err != nil {
return err
}
if p == nil {
if cfg.Server.IsProd || cfg.Server.StrictAttestors {
- panic("no provider entry for " + providerName)
+ panic("no provider entry for " + provider.Name)
} else {
- fmt.Println("non-strict attestor initialization. skipping", providerName)
+ fmt.Println("non-strict attestor initialization. skipping", provider)
continue
}
}
diff --git a/c2ec/model.go b/c2ec/model.go
@@ -49,6 +49,13 @@ func ToWithdrawalOpStatus(s string) (WithdrawalOperationStatus, error) {
}
}
+func WopidValid(wopid string) bool {
+
+ buf := make([]byte, 32)
+ res, err := TalerBase32Decode(wopid, buf)
+ return err != nil && res == 32
+}
+
type ErrorDetail struct {
// Numeric error code unique to the condition.
diff --git a/c2ec/wire-gateway.go b/c2ec/wire-gateway.go
@@ -155,6 +155,7 @@ func transfer(res http.ResponseWriter, req *http.Request) {
return
}
+ // prepare the body to add or compare.
body := make([]byte, req.ContentLength)
_, err = req.Body.Read(body)
if err != nil {
diff --git a/cli/README b/cli/README
@@ -0,0 +1,15 @@
+# C2EC Management CLI
+
+This allows adding Providers and Terminals to the database using the command line.
+
+Before using the commands which connect to the database, you first need to connect to the database (can be done within the CLI using the `db` command).
+
+It will take care of generating the access tokens for the terminal and hashing the authorization key of the provider backend.
+
+## Build
+
+`go build .`
+
+## Run
+
+`./c2ec-cli`
+\ No newline at end of file
diff --git a/cli/cli.go b/cli/cli.go
@@ -0,0 +1,254 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/jackc/pgx/v5"
+ "golang.org/x/crypto/argon2"
+)
+
+const ACTION_HELP = "h"
+const ACTION_REGISTER_PROVIDER = "rp"
+const ACTION_REGISTER_TERMINAL = "rt"
+const ACTION_CONNECT_DB = "db"
+const ACTION_QUIT = "q"
+
+// format of wallee backend credentials.
+type WalleeCredentials struct {
+ SpaceId int `json:"spaceId"`
+ UserId int `json:"userId"`
+ ApplicationUserKey string `json:"application-user-key"`
+}
+
+var DB *pgx.Conn
+
+// enter database credentials (host, port, database, username, password)
+// register terminal -> read password, hash, save to database
+// register provider -> read wallee, read space(id), read userid, read password, hash what needs to be hashed, save to database
+
+func main() {
+ fmt.Println("What do you want to do?")
+ showHelp()
+ for {
+ err := dispatchCommand(read("Type command (term in brackets): "))
+ if err != nil {
+ fmt.Println("Error occured:", err.Error())
+ }
+ }
+}
+
+func registerWalleeProvider() error {
+
+ if DB == nil {
+ return errors.New("connect to the database first (cmd: db)")
+ }
+
+ name := "Wallee"
+ paytotargettype := "wallee-transaction"
+ backendUrl := read("Wallee backend base url: ")
+ spaceIdStr := read("Wallee Space Id: ")
+ spaceId, err := strconv.Atoi(spaceIdStr)
+ if err != nil {
+ return err
+ }
+ userIdStr := read("Wallee User Id: ")
+ userId, err := strconv.Atoi(userIdStr)
+ if err != nil {
+ return err
+ }
+ key := read("Wallee Application User Key: ")
+ hashedKey, err := pbkdf(key)
+ if err != nil {
+ return err
+ }
+
+ creds, err := NewJsonCodec[WalleeCredentials]().EncodeToBytes(&WalleeCredentials{
+ SpaceId: spaceId,
+ UserId: userId,
+ ApplicationUserKey: hashedKey,
+ })
+ if err != nil {
+ return err
+ }
+ credsEncoded := base64.StdEncoding.EncodeToString(creds)
+
+ rows, err := DB.Query(
+ context.Background(),
+ INSERT_PROVIDER,
+ name,
+ paytotargettype,
+ backendUrl,
+ credsEncoded,
+ )
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ return nil
+}
+
+func registerWalleeTerminal() error {
+
+ if DB == nil {
+ return errors.New("connect to the database first (cmd: db)")
+ }
+
+ description := read("Description (location, inventory identifier, etc.): ")
+ providerName := read("Provider Name: ")
+
+ rows, err := DB.Query(
+ context.Background(),
+ GET_PROVIDER_BY_NAME,
+ providerName,
+ )
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ p, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Provider])
+ if err != nil {
+ return err
+ }
+
+ accessToken := make([]byte, 32)
+ _, err = rand.Read(accessToken)
+ if err != nil {
+ return err
+ }
+
+ accessTokenBase64 := base64.StdEncoding.EncodeToString(accessToken)
+ fmt.Println("GENERATED ACCESS-TOKEN:", accessTokenBase64)
+
+ _, err = DB.Query(
+ context.Background(),
+ INSERT_TERMINAL,
+ accessToken,
+ description,
+ p.ProviderTerminalID,
+ )
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ return nil
+}
+
+func connectDatabase() error {
+
+ u := read("Username: ")
+ pw := read("Password: ")
+ h := read("Host: ")
+ ps := read("Port: ")
+ p, err := strconv.Atoi(ps)
+ if err != nil {
+ return err
+ }
+ d := read("Database: ")
+
+ connstring := PostgresConnectionString(u, pw, h, p, d)
+ dbCfg, err := pgx.ParseConfig(connstring)
+ if err != nil {
+ return err
+ }
+
+ DB, err = pgx.ConnectConfig(context.Background(), dbCfg)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func showHelp() error {
+
+ fmt.Println("register wallee provider (", ACTION_REGISTER_PROVIDER, ")")
+ fmt.Println("register wallee terminal (", ACTION_REGISTER_TERMINAL, ")")
+ fmt.Println("connect database (", ACTION_CONNECT_DB, ")")
+ fmt.Println("show help (", ACTION_HELP, ")")
+ fmt.Println("quit (", ACTION_QUIT, ")")
+ return nil
+}
+
+func quit() error {
+ fmt.Println("bye...")
+ os.Exit(0)
+ return nil
+}
+
+func pbkdf(pw string) (string, error) {
+
+ rfcTime := 3
+ rfcMemory := 32 * 1024
+ salt := make([]byte, 16)
+ _, err := rand.Read(salt)
+ if err != nil {
+ return "", err
+ }
+ key := argon2.Key([]byte(pw), salt, uint32(rfcTime), uint32(rfcMemory), 4, 32)
+
+ keyAndSalt := make([]byte, 0, 48)
+ keyAndSalt = append(keyAndSalt, key...)
+ keyAndSalt = append(keyAndSalt, salt...)
+ if len(keyAndSalt) != 48 {
+ return "", errors.New("invalid password hash and salt")
+ }
+ return base64.StdEncoding.EncodeToString(keyAndSalt), nil
+}
+
+func PostgresConnectionString(
+ username string,
+ password string,
+ host string,
+ port int,
+ database string,
+) string {
+ return fmt.Sprintf(
+ "postgres://%s:%s@%s:%d/%s",
+ username,
+ password,
+ host,
+ port,
+ database,
+ )
+}
+
+func dispatchCommand(cmd string) error {
+
+ var err error
+ switch cmd {
+ case ACTION_HELP:
+ err = showHelp()
+ case ACTION_QUIT:
+ err = quit()
+ case ACTION_CONNECT_DB:
+ err = connectDatabase()
+ case ACTION_REGISTER_PROVIDER:
+ err = registerWalleeProvider()
+ case ACTION_REGISTER_TERMINAL:
+ err = registerWalleeTerminal()
+ default:
+ fmt.Println("unknown action")
+ }
+ return err
+}
+
+func read(prefix string) string {
+ reader := bufio.NewReader(os.Stdin)
+ fmt.Print(prefix)
+ inp, err := reader.ReadString('\n')
+ if err != nil {
+ fmt.Println(err.Error())
+ return ""
+ }
+ return strings.Trim(inp, "\n")
+}
diff --git a/cli/codec.go b/cli/codec.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+)
+
+type Codec[T any] interface {
+ HttpApplicationContentHeader() string
+ Encode(*T) (io.Reader, error)
+ EncodeToBytes(body *T) ([]byte, error)
+ Decode(io.Reader) (*T, error)
+}
+
+type JsonCodec[T any] struct {
+ Codec[T]
+}
+
+func NewJsonCodec[T any]() Codec[T] {
+
+ return new(JsonCodec[T])
+}
+
+func (*JsonCodec[T]) Encode(body *T) (io.Reader, error) {
+
+ encodedBytes, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+
+ return bytes.NewReader(encodedBytes), err
+}
+
+func (c *JsonCodec[T]) EncodeToBytes(body *T) ([]byte, error) {
+
+ reader, err := c.Encode(body)
+ if err != nil {
+ return make([]byte, 0), err
+ }
+ buf, err := io.ReadAll(reader)
+ if err != nil {
+ return make([]byte, 0), err
+ }
+ return buf, nil
+}
+
+func (*JsonCodec[T]) Decode(reader io.Reader) (*T, error) {
+
+ body := new(T)
+ err := json.NewDecoder(reader).Decode(body)
+ return body, err
+}
diff --git a/cli/db.go b/cli/db.go
@@ -0,0 +1,21 @@
+package main
+
+const INSERT_PROVIDER = "INSERT INTO c2ec.provider (name, payto_target_type, backend_base_url, backend_credentials) VALUES ($1,$2,$3,$4)"
+const INSERT_TERMINAL = "INSERT INTO c2ec.terminal (access_token, description, provider_id) VALUES ($1,$2,$3)"
+const GET_PROVIDER_BY_NAME = "SELECT * FROM c2ec.provider WHERE name=$1"
+
+type Provider struct {
+ ProviderTerminalID int64 `db:"provider_id"`
+ Name string `db:"name"`
+ PaytoTargetType string `db:"payto_target_type"`
+ BackendBaseURL string `db:"backend_base_url"`
+ BackendCredentials string `db:"backend_credentials"`
+}
+
+type Terminal struct {
+ TerminalID int64 `db:"terminal_id"`
+ AccessToken []byte `db:"access_token"`
+ Active bool `db:"active"`
+ Description string `db:"description"`
+ ProviderID int64 `db:"provider_id"`
+}
diff --git a/cli/go.mod b/cli/go.mod
@@ -0,0 +1,15 @@
+module c2ec-cli
+
+go 1.22.1
+
+require (
+ github.com/jackc/pgx/v5 v5.5.5
+ golang.org/x/crypto v0.22.0
+)
+
+require (
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ golang.org/x/sys v0.19.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+)
diff --git a/cli/go.sum b/cli/go.sum
@@ -0,0 +1,30 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/docs/content/implementation/c2ec-db.tex b/docs/content/implementation/c2ec-db.tex
@@ -0,0 +1,15 @@
+\subsection{Database}
+
+The Database is implemented using Postgresql. This database is also used by other Taler components and therefore is a good fit.
+
+Besides the standard SQL features to insert and select data, Postgres also comes with handy features like LISTEN and NOTIFY.
+
+This allows the implementation of neat pub/sub models allowing better performance and separation of concerns.
+
+\subsubsection{Schema}
+
+\subsubsection{Triggers}
+
+Two types of triggers are implemented. One type notifies listeners about the update of a withdrawal entry. The trigger runs every time an entry is created using INSERT statements and every time a change of the withdrawal status field is detected.
+
+The trigger runs a Postgres function which will execute a NOTIFY statement using Postgres built-in function \textit{pg_notify}, which wraps the statement in a Postgres function allowing to be used more easy.
+\ No newline at end of file
diff --git a/docs/content/implementation/c2ec.tex b/docs/content/implementation/c2ec.tex
@@ -1,5 +1,5 @@
\section{C2EC}
-\subsection{Database}
+\include{content/implementation/c2ec-db}
\subsection{Server}
diff --git a/docs/thesis.pdf b/docs/thesis.pdf
Binary files differ.
diff --git a/specs/api-c2ec.rst b/specs/api-c2ec.rst
@@ -0,0 +1,342 @@
+..
+ This file is part of GNU TALER.
+
+ Copyright (C) 2014-2024 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 2.1, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Joel Häberli
+
+===========================
+The C2EC RESTful API
+===========================
+
+.. note::
+
+ **This API is experimental and not yet implemented**
+
+This chapter describe the APIs that third party providers need to integrate to allow
+withdrawals through indirect payment channels like credit cards or ATM.
+
+.. contents:: Table of Contents
+
+--------------
+Authentication
+--------------
+
+Terminals which authenticate against the C2EC API must provide their respective
+access token. Therefore they provide a ``Authorization: Bearer $ACCESS_TOKEN`` header,
+where `$ACCESS_TOKEN`` is a secret authentication token configured by the exchange and
+must begin with the RFC 8959 prefix.
+
+----------------------------
+Configuration of C2EC
+----------------------------
+
+.. http:get:: /config
+
+ Return the protocol version and configuration information about the C2EC API.
+
+ **Response:**
+
+ :http:statuscode:`200 OK`:
+ The exchange responds with a `C2ECConfig` object. This request should
+ virtually always be successful.
+
+ **Details:**
+
+ .. ts:def:: C2ECConfig
+
+ interface C2ECConfig {
+ // Name of the API.
+ name: "taler-c2ec";
+
+ // libtool-style representation of the C2EC protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+ }
+
+-----------------------------
+Withdrawing using C2EC
+-----------------------------
+
+Withdrawals with a C2EC are based on withdrawal operations which register a withdrawal identifier
+(nonce) at the C2EC component. The provider must first create a unique identifier for the withdrawal
+operation (the ``WOPID``) to interact with the withdrawal operation and eventually withdraw using the wallet.
+
+.. http:post:: /withdrawal-operation
+
+ Register a `WOPID` belonging to a reserve public key.
+
+ **Request:**
+
+ .. ts:def:: C2ECWithdrawRegistration
+
+ interface C2ECWithdrawRegistration {
+ // Maps a nonce generated by the provider to a reserve public key generated by the wallet.
+ wopid: ShortHashCode;
+
+ // Reserve public key generated by the wallet.
+ // According to TALER_ReservePublicKeyP (https://docs.taler.net/core/api-common.html#cryptographic-primitives)
+ reserve_pub_key: EddsaPublicKey;
+
+ // Optional amount for the withdrawal.
+ amount?: Amount;
+
+ // Id of the terminal of the provider requesting a withdrawal by nonce.
+ // Assigned by the exchange.
+ terminal_id: SafeUint64;
+ }
+
+ **Response:**
+
+ :http:statuscode:`204 No content`:
+ The withdrawal was successfully registered.
+ :http:statuscode:`400 Bad request`:
+ The ``WithdrawRegistration`` request was malformed or contained invalid parameters.
+ :http:statuscode:`500 Internal Server error`:
+ The registration of the withdrawal failed due to server side issues.
+
+.. http:get:: /withdrawal-operation/$WOPID
+
+ Query information about a withdrawal operation, identified by the ``WOPID``.
+
+ **Request:**
+
+ :query long_poll_ms:
+ *Optional.* If specified, the bank will wait up to ``long_poll_ms``
+ milliseconds for operationt state to be different from ``old_state`` before sending the HTTP
+ response. A client must never rely on this behavior, as the bank may
+ return a response immediately.
+ :query old_state:
+ *Optional.* Default to "pending".
+
+ **Response:**
+
+ :http:statuscode:`200 Ok`:
+ The withdrawal was found and is returned in the response body as ``C2ECWithdrawalStatus``.
+ :http:statuscode:`404 Not found`:
+ C2EC does not have a withdrawal registered with the specified ``WOPID``.
+
+ **Details**
+
+ .. ts:def:: C2ECWithdrawalStatus
+
+ interface C2ECWithdrawalStatus {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ // Since protocol v1.
+ status: "pending" | "selected" | "aborted" | "confirmed";
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations).
+ amount: Amount;
+
+ // A refund address as ``payto`` URI. This address shall be used
+ // in case a refund must be done. Only not-null if the status
+ // is "confirmed" or "aborted"
+ sender_wire?: string;
+
+ // Reserve public key selected by the exchange,
+ // only non-null if ``status`` is ``selected`` or ``confirmed``.
+ // Since protocol v1.
+ selected_reserve_pub?: string;
+ }
+
+
+.. http:post:: /withdrawal-operation/$WOPID
+
+ Notifies C2EC about an executed payment for a specific withdrawal.
+
+ **Request:**
+
+ .. ts:def:: C2ECPaymentNotification
+
+ interface C2ECPaymentNotification {
+
+ // Unique identifier of the provider transaction.
+ provider_transaction_id: string;
+
+ // Specifies the amount which was payed to the provider (without fees).
+ // This amount shall be put into the reserve linked to by the withdrawal id.
+ amount: Amount;
+
+ // Fees associated with the payment.
+ fees: Amount;
+ }
+
+ **Response:**
+
+ :http:statuscode:`204 No content`:
+ C2EC received the ``C2ECPaymentNotification`` successfully and will further process
+ the withdrawal.
+ :http:statuscode:`400 Bad request`:
+ The ``C2ECPaymentNotification`` request was malformed or contained invalid parameters.
+ :http:statuscode:`404 Not found`:
+ C2EC does not have a withdrawal registered with the specified ``WOPID``.
+ :http:statuscode:`500 Internal Server error`:
+ The ``C2ECPaymentNotification`` could not be processed due to server side issues.
+
+
+--------------
+Taler Wire Gateway
+--------------
+
+C2EC implements the wire gateway API in order to check for incoming transactions and
+let the exchange get proofs of payments. This will allow the C2EC componente to add reserves
+and therefore allow the withdrawal of the money. C2EC does not entirely implement all endpoints,
+because the it is not needed for the case of C2EC. The endpoints not implemented are not described
+further. They will be available but respond with 400 http error code.
+
+.. http:get:: /config
+
+ Return the protocol version and configuration information about the bank.
+ This specification corresponds to ``current`` protocol being version **0**.
+
+ **Response:**
+
+ :http:statuscode:`200 OK`:
+ The exchange responds with a `WireConfig` object. This request should
+ virtually always be successful.
+
+ **Details:**
+
+ .. ts:def:: WireConfig
+
+ interface WireConfig {
+ // Name of the API.
+ name: "taler-wire-gateway";
+
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this gateway.
+ currency: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+.. http:post:: /transfer
+
+ This API allows the exchange to make a transaction, typically to a merchant. The bank account
+ of the exchange is not included in the request, but instead derived from the user name in the
+ authentication header and/or the request base URL.
+
+ To make the API idempotent, the client must include a nonce. Requests with the same nonce
+ are rejected unless the request is the same.
+
+ **Request:**
+
+ .. ts:def:: TransferRequest
+
+ interface TransferRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // ``request_uid`` that differ in any of the other fields
+ // are rejected.
+ request_uid: HashCode;
+
+ // Amount to transfer.
+ amount: Amount;
+
+ // Base URL of the exchange. Shall be included by the bank gateway
+ // in the appropriate section of the wire transfer details.
+ exchange_base_url: string;
+
+ // Wire transfer identifier chosen by the exchange,
+ // used by the merchant to identify the Taler order(s)
+ // associated with this wire transfer.
+ wtid: ShortHashCode;
+
+ // The recipient's account identifier as a payto URI.
+ credit_account: string;
+ }
+
+ **Response:**
+
+ :http:statuscode:`200 OK`:
+ The request has been correctly handled, so the funds have been transferred to
+ the recipient's account. The body is a `TransferResponse`.
+ :http:statuscode:`400 Bad request`:
+ Request malformed. The bank replies with an `ErrorDetail` object.
+ :http:statuscode:`401 Unauthorized`:
+ Authentication failed, likely the credentials are wrong.
+ :http:statuscode:`404 Not found`:
+ The endpoint is wrong or the user name is unknown. The bank replies with an `ErrorDetail` object.
+ :http:statuscode:`409 Conflict`:
+ A transaction with the same ``request_uid`` but different transaction details
+ has been submitted before.
+
+ **Details:**
+
+ .. ts:def:: TransferResponse
+
+ interface TransferResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+ }
+
+.. http:get:: /history/incoming
+
+ **Request:**
+
+ :query start: *Optional.*
+ Row identifier to explicitly set the *starting point* of the query.
+ :query delta:
+ The *delta* value that determines the range of the query.
+ :query long_poll_ms: *Optional.* If this parameter is specified and the
+ result of the query would be empty, the bank will wait up to ``long_poll_ms``
+ milliseconds for new transactions that match the query to arrive and only
+ then send the HTTP response. A client must never rely on this behavior, as
+ the bank may return a response immediately or after waiting only a fraction
+ of ``long_poll_ms``.
+
+ **Response:**
+
+ .. ts:def:: IncomingReserveTransaction
+
+ interface IncomingReserveTransaction {
+ type: "RESERVE";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: Amount;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: string;
+
+ // The reserve public key extracted from the transaction details.
+ reserve_pub: EddsaPublicKey;
+
+ }
+