commit 8ef1cff6b7c441f7d3eae2876299700b62f08bac
parent fbf33892e87b4888cfa5bf1882ada66b19b32cbb
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Tue, 11 Nov 2025 15:32:45 +0100
revamp API (again), add paid mailbox creation
Diffstat:
4 files changed, 397 insertions(+), 242 deletions(-)
diff --git a/cmd/mailbox-server/main_test.go b/cmd/mailbox-server/main_test.go
@@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "strconv"
"strings"
"testing"
"time"
@@ -62,10 +63,17 @@ func executeRequest(req *http.Request) *httptest.ResponseRecorder {
return rr
}
-func checkResponseCode(t *testing.T, expected, actual int) {
+func checkResponseCode(t *testing.T, expected, actual int) bool {
if expected != actual {
t.Errorf("Expected response code %d, Got %d\n", expected, actual)
}
+ return expected == actual
+}
+
+var merchServerRespondsPaid = false
+
+func shouldReturnPaid() bool {
+ return merchServerRespondsPaid
}
func TestMain(m *testing.M) {
@@ -91,11 +99,12 @@ func TestMain(m *testing.M) {
if err != nil {
fmt.Printf("Error %s\n", err)
}
- jsonResp := fmt.Sprintf("{\"order_id\":\"%s\"}", orderResp.Order.OrderId)
+ jsonResp := fmt.Sprintf("{\"order_id\":\"%s\"}", "uniqueOrderId")
w.WriteHeader(http.StatusOK)
w.Write([]byte(jsonResp))
} else {
- if r.Header.Get("PaidIndicator") == "yes" {
+ fmt.Printf("Responding always paid: %v\n", merchServerRespondsPaid)
+ if shouldReturnPaid() {
jsonResp := "{\"order_status\":\"paid\"}"
w.WriteHeader(http.StatusOK)
w.Write([]byte(jsonResp))
@@ -159,35 +168,35 @@ func TestGetKeysEmpty(t *testing.T) {
checkResponseCode(t, http.StatusNotFound, response.Code)
}
-func TestPostKeys(t *testing.T) {
- var msg mailbox.KeyUpdateRequest
+func TestMailboxRegistration(t *testing.T) {
+ var msg mailbox.MailboxRegistrationRequest
// Dummy pubkey
encKey := make([]byte, 32)
aliceSigningKey := util.Base32CrockfordEncode(testAliceSigningKey)
- msg.Keys.EncryptionKey = util.Base32CrockfordEncode(encKey)
- msg.Keys.EncryptionKeyType = "X25519"
- msg.Keys.Expiration = mailbox.Timestamp{T_s: uint64(time.Now().UnixMicro())}
- msg.Keys.SigningKey = aliceSigningKey
- msg.Keys.SigningKeyType = "EdDSA"
+ msg.MailboxMetadata.EncryptionKey = util.Base32CrockfordEncode(encKey)
+ msg.MailboxMetadata.EncryptionKeyType = "X25519"
+ msg.MailboxMetadata.Expiration = mailbox.Timestamp{T_s: uint64(time.Now().Add(time.Hour * 24 * 365).UnixMilli() / 1000)}
+ msg.MailboxMetadata.SigningKey = aliceSigningKey
+ msg.MailboxMetadata.SigningKeyType = "EdDSA"
expNbo := make([]byte, 8)
- binary.BigEndian.PutUint64(expNbo, msg.Keys.Expiration.T_s)
+ binary.BigEndian.PutUint64(expNbo, msg.MailboxMetadata.Expiration.T_s)
h := sha512.New()
- h.Write([]byte(msg.Keys.EncryptionKeyType))
+ h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType))
h.Write(encKey)
h.Write(expNbo)
- var signed_msg [64 + 4 + 4]byte
- size := signed_msg[0:4]
+ var signedMsg [64 + 4 + 4]byte
+ size := signedMsg[0:4]
binary.BigEndian.PutUint32(size, 64+4+4)
- purp := signed_msg[4:8]
- binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_KEYS_UPDATE)
- copy(signed_msg[8:], h.Sum(nil))
- sig := ed25519.Sign(testAliceSigningKeyPriv, signed_msg[0:])
- if !ed25519.Verify(testAliceSigningKey, signed_msg[0:], sig) {
+ purp := signedMsg[4:8]
+ binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
+ copy(signedMsg[8:], h.Sum(nil))
+ sig := ed25519.Sign(testAliceSigningKeyPriv, signedMsg[0:])
+ if !ed25519.Verify(testAliceSigningKey, signedMsg[0:], sig) {
t.Errorf("Signature invalid!")
}
msg.Signature = util.Base32CrockfordEncode(sig)
jsonMsg, _ := json.Marshal(msg)
- req, _ := http.NewRequest("POST", "/keys", bytes.NewReader(jsonMsg))
+ req, _ := http.NewRequest("POST", "/register", bytes.NewReader(jsonMsg))
response := executeRequest(req)
checkResponseCode(t, http.StatusNoContent, response.Code)
req, _ = http.NewRequest("GET", "/keys/"+testAliceHashedSigningKeyString, nil)
@@ -198,49 +207,95 @@ func TestPostKeys(t *testing.T) {
t.Errorf("Expected response, Got %s", body)
return
}
- var respMsg mailbox.MailboxMessageKeys
+ var respMsg mailbox.MailboxMetadata
err := json.NewDecoder(response.Body).Decode(&respMsg)
if err != nil {
- fmt.Printf("Error %s\n", err)
+ t.Errorf("Error %s\n", err)
+ }
+ if respMsg.SigningKey != msg.MailboxMetadata.SigningKey {
+ fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
}
- if respMsg != msg.Keys {
- fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.Keys)
+ if respMsg.EncryptionKey != msg.MailboxMetadata.EncryptionKey {
+ fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
}
+ a.Db.Where("1 = 1").Delete(&mailbox.MailboxMetadata{})
+ a.Db.Where("1 = 1").Delete(&mailbox.PendingMailboxRegistration{})
}
-func TestPostMessagePaid(t *testing.T) {
- testMessage := make([]byte, 256)
+
+func TestMailboxRegistrationPaid(t *testing.T) {
+ var msg mailbox.MailboxRegistrationRequest
// Make paid
- fee, err := talerutil.ParseAmount("KUDOS:1")
- if err != nil {
- t.Errorf("%v", err)
+ registrationUpdateFee := talerutil.NewAmount("KUDOS", 1, 0)
+ monthlyFee := talerutil.NewAmount("KUDOS", 2, 0)
+ a.RegistrationUpdateFee = ®istrationUpdateFee
+ a.MonthlyFee = &monthlyFee
+
+ // Dummy pubkey
+ encKey := make([]byte, 32)
+ aliceSigningKey := util.Base32CrockfordEncode(testAliceSigningKey)
+ msg.MailboxMetadata.EncryptionKey = util.Base32CrockfordEncode(encKey)
+ msg.MailboxMetadata.EncryptionKeyType = "X25519"
+ msg.MailboxMetadata.Expiration = mailbox.Timestamp{T_s: uint64(time.Now().Add(time.Hour * 24 * 365).UnixMicro())}
+ msg.MailboxMetadata.SigningKey = aliceSigningKey
+ msg.MailboxMetadata.SigningKeyType = "EdDSA"
+ expNbo := make([]byte, 8)
+ binary.BigEndian.PutUint64(expNbo, msg.MailboxMetadata.Expiration.T_s)
+ h := sha512.New()
+ h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType))
+ h.Write(encKey)
+ h.Write(expNbo)
+ var signedMsg [64 + 4 + 4]byte
+ size := signedMsg[0:4]
+ binary.BigEndian.PutUint32(size, 64+4+4)
+ purp := signedMsg[4:8]
+ binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
+ copy(signedMsg[8:], h.Sum(nil))
+ sig := ed25519.Sign(testAliceSigningKeyPriv, signedMsg[0:])
+ if !ed25519.Verify(testAliceSigningKey, signedMsg[0:], sig) {
+ t.Errorf("Signature invalid!")
}
- a.MessageFee = fee
- a.Db.Where("1 = 1").Delete(&mailbox.InboxEntry{})
- req, _ := http.NewRequest("POST", "/"+testAliceHashedSigningKeyString, bytes.NewReader(testMessage))
+ msg.Signature = util.Base32CrockfordEncode(sig)
+ jsonMsg, _ := json.Marshal(msg)
+ req, _ := http.NewRequest("POST", "/register", bytes.NewReader(jsonMsg))
response := executeRequest(req)
-
checkResponseCode(t, http.StatusPaymentRequired, response.Code)
- // TODO check QR / payto response
+ req, _ = http.NewRequest("GET", "/keys/"+testAliceHashedSigningKeyString, nil)
+ response = executeRequest(req)
+ checkResponseCode(t, http.StatusNotFound, response.Code)
- req, _ = http.NewRequest("POST", "/"+testAliceHashedSigningKeyString, bytes.NewReader(testMessage))
- req.Header.Add("PaidIndicator", "yes")
+ merchServerRespondsPaid = true
+ req, _ = http.NewRequest("GET", "/keys/"+testAliceHashedSigningKeyString, nil)
response = executeRequest(req)
+ checkResponseCode(t, http.StatusOK, response.Code)
- checkResponseCode(t, http.StatusPaymentRequired, response.Code)
+ req, _ = http.NewRequest("GET", "/keys/"+testAliceHashedSigningKeyString, nil)
+ response = executeRequest(req)
+ merchServerRespondsPaid = false
+ checkResponseCode(t, http.StatusOK, response.Code)
body := response.Body.String()
- if body != "" {
- t.Errorf("Expected empty response, Got %s", body)
+ if body == "" {
+ t.Errorf("Expected response, Got %s", body)
+ return
+ }
+ var respMsg mailbox.MailboxMetadata
+ err := json.NewDecoder(response.Body).Decode(&respMsg)
+ if err != nil {
+ fmt.Printf("Error %s\n", err)
+ }
+ if respMsg.SigningKey != msg.MailboxMetadata.SigningKey {
+ fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
+ }
+ if respMsg.EncryptionKey != msg.MailboxMetadata.EncryptionKey {
+ fmt.Printf("Keys mismatch! %v %v\n", respMsg, msg.MailboxMetadata)
}
- a.MessageFee, _ = talerutil.ParseAmount("KUDOS:0")
}
func TestPostThenDeleteMessage(t *testing.T) {
// testMessage := make([]byte, 256)
- var deletionReq mailbox.MessageDeletionRequest
numMessagesToPost := (a.MessageResponseLimit + 7)
testMessages := make([]byte, 256*numMessagesToPost)
_, _ = rand.Read(testMessages)
@@ -279,25 +334,20 @@ func TestPostThenDeleteMessage(t *testing.T) {
for i := 0; i < int(a.MessageResponseLimit); i++ {
h.Write(testMessages[i*256 : (i+1)*256])
}
- var signed_msg [64 + 4 + 4]byte
- size := signed_msg[0:4]
- binary.BigEndian.PutUint32(size, 64+4+4)
- purp := signed_msg[4:8]
- binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_MESSAGES_DELETE)
- checksum := h.Sum(nil)
- copy(signed_msg[8:], checksum)
- sig := ed25519.Sign(testAliceSigningKeyPriv, signed_msg[0:])
- if !ed25519.Verify(testAliceSigningKey, signed_msg[0:], sig) {
+ etagInt, _ := strconv.Atoi(etag)
+ var signedMsg [4 * 4]byte
+ binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
+ binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
+ binary.BigEndian.PutUint32(signedMsg[8:12], uint32(etagInt))
+ binary.BigEndian.PutUint32(signedMsg[12:16], uint32(a.MessageResponseLimit))
+ sig := ed25519.Sign(testAliceSigningKeyPriv, signedMsg[0:])
+ if !ed25519.Verify(testAliceSigningKey, signedMsg[0:], sig) {
t.Errorf("Signature invalid!")
}
- deletionReq.Signature = util.Base32CrockfordEncode(sig)
- deletionReq.Count = int(a.MessageResponseLimit)
- deletionReq.Checksum = util.Base32CrockfordEncode(checksum)
- reqJson, _ := json.Marshal(deletionReq)
-
hAddress := util.Base32CrockfordEncode(testAliceSigningKey)
- req, _ = http.NewRequest("DELETE", "/"+hAddress, bytes.NewBuffer(reqJson))
+ req, _ = http.NewRequest("DELETE", "/"+hAddress+"?count="+strconv.Itoa(int(a.MessageResponseLimit)), nil)
req.Header.Add("If-Match", etag)
+ req.Header.Add("Taler-Mailbox-Delete-Signature", util.Base32CrockfordEncode(sig))
response = executeRequest(req)
checkResponseCode(t, http.StatusNoContent, response.Code)
diff --git a/cmd/mailbox-server/test-mailbox.conf b/cmd/mailbox-server/test-mailbox.conf
@@ -2,8 +2,10 @@
bind_to = localhost:11000
production = false
message_body_bytes = 256
-message_fee = KUDOS:0
+monthly_fee = KUDOS:0
+registration_update_fee = KUDOS:0
message_response_limit = 25
+pending_registration_expiration = 24h
[mailbox-pq]
host = localhost
diff --git a/mailbox.conf.example b/mailbox.conf.example
@@ -4,8 +4,7 @@ base_url = https://example.com
production = false
message_body_bytes = 256
message_response_limit = 50
-registration_fee = KUDOS:0
-registration_period = 8760h
+monthly_fee = KUDOS:0
[mailbox-pq]
host = localhost
diff --git a/pkg/rest/mailbox.go b/pkg/rest/mailbox.go
@@ -19,9 +19,7 @@
package mailbox
import (
- "bytes"
"crypto/ed25519"
- "crypto/sha256"
"crypto/sha512"
"encoding/binary"
"encoding/json"
@@ -108,8 +106,11 @@ type Mailbox struct {
// Base URL
BaseUrl string
- // Registration fee for registering mailbox
- RegistrationFee *talerutil.Amount
+ // Registration fee for each validity month mailbox
+ MonthlyFee *talerutil.Amount
+
+ // Registration fee for registering or modifying mailbox
+ RegistrationUpdateFee *talerutil.Amount
// How many messages will a single response
// contain at maximum.
@@ -121,8 +122,6 @@ type Mailbox struct {
// Currency Spec
CurrencySpec talerutil.CurrencySpecification
- // Registration period
- RegistrationPeriod time.Duration
}
type RelativeTime struct {
@@ -133,6 +132,10 @@ type Timestamp struct {
T_s uint64 `json:"t_s"`
}
+// 1 Month as Go duration
+const monthDuration = time.Hour * 24 * 30
+
+
// VersionResponse is the JSON response of the /config endpoint
type VersionResponse struct {
// libtool-style representation of the Mailbox protocol version, see
@@ -155,16 +158,13 @@ type VersionResponse struct {
MessageResponseLimit uint64 `json:"message_response_limit"`
// How much is the cost of a single
- // registration period of a mailbox
+ // registration month of a mailbox
// May be 0 for a free registration.
- RegistrationFee string `json:"registration_fee"`
+ MonthlyFee string `json:"registration_fee"`
- // How long is a registration period
- // a mailbox
- RegistrationPeriod int64 `json:"registration_period"`
}
-type MailboxMessageKeys struct {
+type MailboxMetadata struct {
// ORM
gorm.Model `json:"-"`
@@ -198,12 +198,25 @@ type MailboxMessageKeys struct {
Expiration Timestamp `json:"expiration" gorm:"embedded;embeddedPrefix:expiration_"`
}
-type KeyUpdateRequest struct {
+type PendingMailboxRegistration struct {
// ORM
gorm.Model `json:"-"`
+ // Hash of the inbox for this entry
+ HashedSigningKey string // Requested registration duration
+
+ Duration time.Duration
+
+ // The order ID associated with this registration
+ OrderID string `json:"-"`
+
+}
+
+
+type MailboxRegistrationRequest struct {
+
// Keys to add/update for a mailbox.
- Keys MailboxMessageKeys `json:"keys"`
+ MailboxMetadata MailboxMetadata `json:"mailbox_metadata"`
// Signature by the mailbox's signing key affirming
// the update of keys, of purpuse
@@ -213,23 +226,6 @@ type KeyUpdateRequest struct {
Signature string `json:"signature"`
}
-// MessageDeletionRequest is used to request the deletion of already received
-// messages from the mailbox.
-type MessageDeletionRequest struct {
-
- // Number of messages to delete. (Starting from the beginning
- // of the latest GET response).
- Count int
-
- // SHA-512 hash over all messages to delete.
- Checksum string
-
- // Signature by the mailbox's private key affirming
- // the deletion of the messages, of purpose
- // TALER_SIGNATURE_WALLET_MAILBOX_DELETE_MESSAGES.
- Signature string `json:"signature"`
-}
-
// MailboxRateLimitedResponse is the JSON response when a rate limit is hit
type MailboxRateLimitedResponse struct {
@@ -251,7 +247,7 @@ type InboxEntry struct {
Body []byte
// Hash of the inbox for this entry
- HMailbox string
+ HashedSigningKey string
}
func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) {
@@ -265,9 +261,8 @@ func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) {
Name: "taler-mailbox",
MessageBodyBytes: m.MessageBodyBytes,
MessageResponseLimit: m.MessageResponseLimit,
- RegistrationFee: m.RegistrationFee.String(),
+ MonthlyFee: m.MonthlyFee.String(),
DeliveryPeriod: RelativeTime{D_us: uint64(dp.Microseconds())},
- RegistrationPeriod: m.RegistrationPeriod.Microseconds(),
}
w.Header().Set("Content-Type", "application/json")
response, _ := json.Marshal(cfg)
@@ -281,10 +276,11 @@ func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) {
// FIXME rate limit
// FIXME timeout
// FIXME possibly limit results here
- err := m.Db.Where("h_mailbox = ?", vars["h_mailbox"]).Limit(int(m.MessageResponseLimit)).Find(&entries).Error
+ m.checkPendingRegistrationUpdates(vars["h_mailbox"])
+ err := m.Db.Where("hashed_signing_key = ?", vars["h_mailbox"]).Limit(int(m.MessageResponseLimit)).Find(&entries).Error
if err != nil {
log.Printf("%v", err)
- w.WriteHeader(http.StatusInternalServerError)
+ w.WriteHeader(http.StatusNotFound)
return
}
if len(entries) == 0 {
@@ -317,12 +313,13 @@ func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Cannot read body", http.StatusBadRequest)
return
}
- err = m.Db.First(&entry, "h_mailbox = ? AND body = ?", vars["h_mailbox"], body, true).Error
+ m.checkPendingRegistrationUpdates(vars["h_mailbox"])
+ err = m.Db.First(&entry, "hashed_signing_key = ? AND body = ?", vars["h_mailbox"], body, true).Error
if err == nil {
w.WriteHeader(http.StatusNotModified)
return
}
- entry.HMailbox = vars["h_mailbox"]
+ entry.HashedSigningKey = vars["h_mailbox"]
entry.Body = body
m.Db.Save(&entry)
w.WriteHeader(http.StatusNoContent)
@@ -330,18 +327,88 @@ func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
func (m *Mailbox) getKeysResponse(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
- var keyEntry MailboxMessageKeys
+ var keyEntry MailboxMetadata
+ m.checkPendingRegistrationUpdates(vars["h_mailbox"])
err := m.Db.First(&keyEntry, "hashed_signing_key = ?", vars["h_mailbox"]).Error
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
+ m.Logf(LogDebug, "entry expires at %d, have %d", keyEntry.Expiration.T_s, time.Now().Unix())
+ if keyEntry.Expiration.T_s < uint64(time.Now().Unix()) {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
response, _ := json.Marshal(keyEntry)
w.Write(response)
}
-func (m *Mailbox) updateKeysResponse(w http.ResponseWriter, r *http.Request) {
- var msg KeyUpdateRequest
+func (m *Mailbox) validateRegistrationSignature(msg MailboxRegistrationRequest) error {
+ var expNbo [8]byte
+ var signedMsg [72]byte
+ encPk, err := util.Base32CrockfordDecode(msg.MailboxMetadata.EncryptionKey, 32)
+ if err != nil {
+ return fmt.Errorf("unable to decode encryption key")
+ }
+ pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
+ if err != nil {
+ return fmt.Errorf("unable to decode pubkey")
+ }
+ pk := ed25519.PublicKey(pkey)
+ sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
+ if nil != err {
+ return fmt.Errorf("unable to decode signature")
+ }
+ binary.BigEndian.PutUint64(expNbo[:], msg.MailboxMetadata.Expiration.T_s)
+ size := signedMsg[0:4]
+ binary.BigEndian.PutUint32(size, 64+4+4)
+ purp := signedMsg[4:8]
+ binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
+ h := sha512.New()
+ h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType)) // Currently always X25519
+ h.Write(encPk)
+ h.Write(expNbo[:])
+ copy(signedMsg[8:], h.Sum(nil))
+ if !ed25519.Verify(pk, signedMsg[0:], sig) {
+ return fmt.Errorf("signature invalid")
+ }
+ return nil
+}
+
+// Check if this is a non-zero, positive amount
+func calculateCost(sliceCostAmount string, fixedCostAmount string, howLong time.Duration, sliceDuration time.Duration) (*talerutil.Amount, error) {
+ sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds()))
+ sliceCost, err := talerutil.ParseAmount(sliceCostAmount)
+ if nil != err {
+ return nil, err
+ }
+ fixedCost, err := talerutil.ParseAmount(fixedCostAmount)
+ if nil != err {
+ return nil, err
+ }
+ sum := &talerutil.Amount{
+ Currency: sliceCost.Currency,
+ Value: 0,
+ Fraction: 0,
+ }
+ for i := 0; i < sliceCount; i++ {
+ sum, err = sum.Add(*sliceCost)
+ if nil != err {
+ return nil, err
+ }
+ }
+ sum, err = sum.Add(*fixedCost)
+ if nil != err {
+ return nil, err
+ }
+ return sum, nil
+}
+
+
+func (m *Mailbox) registerMailboxResponse(w http.ResponseWriter, r *http.Request) {
+ var msg MailboxRegistrationRequest
+ var pendingRegistration PendingMailboxRegistration
+ var registrationEntry MailboxMetadata
if r.Body == nil {
m.Logf(LogError, "no request body")
http.Error(w, "No request body", http.StatusBadRequest)
@@ -352,140 +419,152 @@ func (m *Mailbox) updateKeysResponse(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Malformed request body", http.StatusBadRequest)
return
}
- pkey, err := util.Base32CrockfordDecode(msg.Keys.SigningKey, 32)
+ pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
if err != nil {
- m.Logf(LogError, "unable to decode pubkey")
- w.WriteHeader(http.StatusBadRequest)
+ http.Error(w, "Public key invalid", http.StatusBadRequest)
return
}
pk := ed25519.PublicKey(pkey)
- sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
+ err = m.validateRegistrationSignature(msg)
if nil != err {
- m.Logf(LogError, "unable to decode signature")
- w.WriteHeader(http.StatusBadRequest)
+ http.Error(w, "Signature verification failed", http.StatusBadRequest)
return
}
- encPk, err := util.Base32CrockfordDecode(msg.Keys.EncryptionKey, 32)
- if err != nil {
- m.Logf(LogError, "unable to decode encryption key")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- if !m.RegistrationFee.IsZero() {
-
- // Check if order exists and was paid already.
- h := sha256.New()
- h.Sum(pk)
- orderID := util.Base32CrockfordEncode(h.Sum(nil))
- httpStatus, paymentStatus, payto, paytoErr := m.Merchant.IsOrderPaid(orderID)
- if paytoErr != nil {
- fmt.Println(paytoErr)
+ h := sha512.New()
+ h.Write(pkey)
+ hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
+ pendingRegistration.HashedSigningKey = hMailbox
+ // Round to the nearest multiple of a month
+ reqExpiration := time.Unix(int64(msg.MailboxMetadata.Expiration.T_s), 0)
+ now := time.Now()
+ reqDuration := reqExpiration.Sub(now).Round(monthDuration)
+ err = m.Db.First(®istrationEntry, "hashed_signing_key = ?", hMailbox).Error
+ if err == nil {
+ // This probably meansthe registration is modified or extended or both
+ entryModified := (registrationEntry.EncryptionKey != msg.MailboxMetadata.EncryptionKey)
+ // At least one MonthlyFee
+ if reqDuration.Microseconds() == 0 && !entryModified {
+ // Nothing changed. Return validity
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+ } else {
+ // Entry does not yet exist, add but immediately expire it
+ registrationEntry = msg.MailboxMetadata
+ registrationEntry.Expiration.T_s = uint64(time.Now().Unix() - 1)
+ hAddr := sha512.New()
+ hAddr.Write(pk)
+ registrationEntry.HashedSigningKey = util.Base32CrockfordEncode(hAddr.Sum(nil))
+ err = m.Db.Create(®istrationEntry).Error
+ if nil != err {
w.WriteHeader(http.StatusInternalServerError)
return
}
- switch httpStatus {
- case http.StatusNotFound:
- // Not found. Create new order.
- var order merchant.CommonOrder
- order.OrderId = orderID
- order.Amount = m.RegistrationFee.String()
- order.Summary = "Mailbox registration"
- order.MerchantBaseUrl = m.BaseUrl
- _, newOrderErr := m.Merchant.CreateOrder(order)
+ }
+ err = m.Db.First(&pendingRegistration, "hashed_signing_key = ?", hMailbox).Error
+ pendingRegistrationExists := (nil == err)
+ if !pendingRegistrationExists {
+ pendingRegistration.HashedSigningKey = hMailbox
+ pendingRegistration.Duration = reqDuration
+ }
+ // At least the update fee needs to be paid
+ cost, err := calculateCost(m.MonthlyFee.String(),
+ m.RegistrationUpdateFee.String(),
+ reqDuration,
+ monthDuration)
+ if err != nil {
+ fmt.Println(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ if !cost.IsZero() {
+ if len(pendingRegistration.OrderID) == 0 {
+ // Add new order
+ orderID, newOrderErr := m.Merchant.AddNewOrder(*cost, "Mailbox registration", m.BaseUrl)
if newOrderErr != nil {
- fmt.Println(newOrderErr)
+ m.Logf(LogError, "%v", newOrderErr)
w.WriteHeader(http.StatusInternalServerError)
return
}
- // Check for order again to get payto.
- _, _, payto, paytoErr = m.Merchant.IsOrderPaid(orderID)
- if paytoErr != nil {
- fmt.Println(paytoErr)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- if payto == "" {
- fmt.Println(paytoErr)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusPaymentRequired)
- w.Header().Set("Taler", payto)
+ m.Logf(LogDebug, "New order ID %s for pending registration for %s", orderID, pendingRegistration.HashedSigningKey)
+ pendingRegistration.OrderID = orderID
+ }
+ // Check if order paid.
+ // FIXME: Remember that it was activated and paid
+ // FIXME: We probably need to handle the return code here (see gns registrar for how)
+ _, _, payto, paytoErr := m.Merchant.IsOrderPaid(pendingRegistration.OrderID)
+ if paytoErr != nil {
+ fmt.Println(paytoErr)
+ w.WriteHeader(http.StatusInternalServerError)
+ m.Logf(LogError, paytoErr.Error()+"\n")
return
- case http.StatusOK:
- // Check if it was actually paid.
- if paymentStatus == "paid" {
- break
- }
- default:
+ }
+ if len(payto) != 0 {
+ m.Db.Save(&pendingRegistration)
w.WriteHeader(http.StatusPaymentRequired)
- if payto != "" {
- w.Header().Set("Taler", payto)
- }
+ w.Header().Set("Taler", payto) // FIXME no idea what to do with this.
return
}
}
- var expNbo [8]byte
- var signedMsg [72]byte
- binary.BigEndian.PutUint64(expNbo[:], msg.Keys.Expiration.T_s)
- size := signedMsg[0:4]
- binary.BigEndian.PutUint32(size, 64+4+4)
- purp := signedMsg[4:8]
- binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
- h := sha512.New()
- h.Write([]byte(msg.Keys.EncryptionKeyType)) // Currently always X25519
- h.Write(encPk)
- h.Write(expNbo[:])
- copy(signedMsg[8:], h.Sum(nil))
- if !ed25519.Verify(pk, signedMsg[0:], sig) {
- w.WriteHeader(http.StatusForbidden)
- return
- }
- var keyEntry MailboxMessageKeys
- keyEntry.SigningKeyType = msg.Keys.SigningKeyType
- keyEntry.SigningKey = msg.Keys.SigningKey
- keyEntry.EncryptionKeyType = msg.Keys.EncryptionKeyType
- keyEntry.EncryptionKey = msg.Keys.EncryptionKey
- keyEntry.Expiration = msg.Keys.Expiration
- hAddr := sha512.New()
- hAddr.Write(pk)
- keyEntry.HashedSigningKey = util.Base32CrockfordEncode(hAddr.Sum(nil))
- err = m.Db.First(&keyEntry, "signing_key = ?", msg.Keys.SigningKey).Error
+ // Update expiration time of registration.
+ registrationEntry.Expiration.T_s += uint64(reqDuration.Seconds())
+ m.Db.Delete(pendingRegistration)
+ err = m.Db.Save(®istrationEntry).Error
if nil != err {
- m.Db.Create(&keyEntry)
- } else {
- err = m.Db.Save(&keyEntry).Error
- if nil != err {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
+ w.WriteHeader(http.StatusInternalServerError)
+ return
}
w.WriteHeader(http.StatusNoContent)
}
-func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- var msg MessageDeletionRequest
- var entries []InboxEntry
- if r.Body == nil {
- http.Error(w, "No request body", 400)
+func (m *Mailbox) checkPendingRegistrationUpdates(hMailbox string) {
+ var pendingEntry PendingMailboxRegistration
+ var registrationEntry MailboxMetadata
+ err := m.Db.First(&pendingEntry, "hashed_signing_key = ?", hMailbox).Error
+ if err != nil {
return
}
- err := json.NewDecoder(r.Body).Decode(&msg)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
+ m.Logf(LogDebug, "Found pending registration for %s, OrderID: %s", hMailbox, pendingEntry.OrderID)
+ rc, orderStatus, _, paytoErr := m.Merchant.IsOrderPaid(pendingEntry.OrderID)
+ if nil != paytoErr {
+ if rc == http.StatusNotFound {
+ m.Logf(LogInfo, "Registration order for `%s' not found, removing\n", hMailbox)
+ }
+ err := m.Db.Delete(&pendingEntry)
+ if nil != err {
+ m.Logf(LogInfo, "%v\n", err)
+ }
+ return
+ }
+ m.Logf(LogDebug, "Order status for %s is %s", pendingEntry.HashedSigningKey, orderStatus)
+ if merchant.OrderPaid == orderStatus {
+ m.Logf(LogDebug, "Order for %v appears to be paid", pendingEntry)
+ err = m.Db.First(®istrationEntry, "hashed_signing_key = ?", hMailbox).Error
+ if err == nil {
+ m.Logf(LogDebug, "Adding %d seconds to entry expiration", uint64(pendingEntry.Duration.Seconds()))
+ registrationEntry.Expiration.T_s += uint64(pendingEntry.Duration.Seconds())
+ m.Db.Save(®istrationEntry)
+ err := m.Db.Delete(&pendingEntry)
+ if nil != err {
+ m.Logf(LogInfo, "%v\n", err)
+ }
+ }
return
}
- etag_hdr := r.Header.Get("If-Match")
- if etag_hdr == "" {
+}
+
+func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ etagHeader := r.Header.Get("If-Match")
+ if etagHeader == "" {
http.Error(w, "If-Match header missing", 400)
return
}
- if strings.Contains(etag_hdr, ",") {
+ if strings.Contains(etagHeader, ",") {
http.Error(w, "If-Match contains multiple values", 400)
return
}
- etag_expected, err := strconv.Atoi(etag_hdr)
+ expectedETag, err := strconv.Atoi(etagHeader)
if err != nil {
http.Error(w, "If-Match contains malformed etag number", 400)
return
@@ -495,52 +574,55 @@ func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request)
w.WriteHeader(http.StatusBadRequest)
return
}
- checksum, err := util.Base32CrockfordDecode(msg.Checksum, 64)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
+ count := 1
+ countStr := r.URL.Query().Get("count")
+ if len(countStr) > 0 {
+ count, err = strconv.Atoi(countStr)
+ if err != nil {
+ http.Error(w, "Malformed count parameter", http.StatusBadRequest)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ headerSig := r.Header["Taler-Mailbox-Delete-Signature"]
+ if nil == headerSig {
+ http.Error(w, "Missing signature", http.StatusBadRequest)
return
}
pk := ed25519.PublicKey(pkey)
- sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
+ sig, err := util.Base32CrockfordDecode(headerSig[0], 64)
if nil != err {
w.WriteHeader(http.StatusBadRequest)
return
}
- var signed_msg [72]byte
- size := signed_msg[0:4]
- binary.BigEndian.PutUint32(size, 64+4+4)
- purp := signed_msg[4:8]
- binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxMessagesDelete)
- copy(signed_msg[8:], checksum)
- if !ed25519.Verify(pk, signed_msg[0:], sig) {
- w.WriteHeader(http.StatusForbidden)
- return
- }
h := sha512.New()
h.Write(pkey)
- h_mailbox := util.Base32CrockfordEncode(h.Sum(nil))
- err = m.Db.Where("h_mailbox = ? AND id >= ?", h_mailbox, etag_expected).Limit(msg.Count).Find(&entries).Error
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
+ hHailbox := util.Base32CrockfordEncode(h.Sum(nil))
+ m.checkPendingRegistrationUpdates(hHailbox)
+ var signedMsg [4 * 4]byte
+ binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
+ binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
+ binary.BigEndian.PutUint32(signedMsg[8:12], uint32(expectedETag))
+ binary.BigEndian.PutUint32(signedMsg[12:16], uint32(count))
+ if !ed25519.Verify(pk, signedMsg[0:], sig) {
+ w.WriteHeader(http.StatusForbidden)
return
}
- if len(entries) != msg.Count {
+ // Check that expectedETag actually exists
+ err = m.Db.Where("hashed_signing_key = ? AND id = ?", hHailbox, expectedETag).Find(&InboxEntry{}).Error
+ if err != nil {
+ m.Logf(LogDebug, "Message to delete not found with ID %d", expectedETag)
w.WriteHeader(http.StatusNotFound)
return
}
- if entries[0].ID != uint(etag_expected) {
- w.WriteHeader(http.StatusPreconditionFailed)
- return
- }
- h_all := sha512.New()
- for _, entry := range entries {
- h_all.Write(entry.Body)
- }
- if !bytes.Equal(h_all.Sum(nil), checksum) {
- w.WriteHeader(http.StatusNotFound)
+ var entries []InboxEntry
+ err = m.Db.Where("hashed_signing_key = ? AND id >= ?", hHailbox, expectedETag).Limit(count).Find(&entries).Error
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
return
}
- m.Db.Delete(&entries)
+ m.Logf(LogDebug, "Found matching ID, deleting %d messages", len(entries))
+ m.Db.Delete(entries)
w.WriteHeader(http.StatusNoContent)
}
@@ -588,8 +670,7 @@ func (m *Mailbox) setupHandlers() {
m.Router.HandleFunc("/config", m.configResponse).Methods("GET")
/* Mailbox API */
- m.Router.HandleFunc("/keys", m.updateKeysResponse).Methods("POST")
- m.Router.HandleFunc("/private/{mailbox}", m.deleteMessagesResponse).Methods("POST")
+ m.Router.HandleFunc("/register", m.registerMailboxResponse).Methods("POST")
m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
@@ -614,19 +695,18 @@ func (m *Mailbox) Initialize(cfg MailboxConfig) {
m.BaseUrl = cfg.Ini.Section("mailbox").Key("base_url").MustString("https://example.com")
m.MessageBodyBytes = cfg.Ini.Section("mailbox").Key("message_body_bytes").MustInt64(256)
m.MessageResponseLimit = cfg.Ini.Section("mailbox").Key("message_response_limit").MustUint64(50)
- registrationPeriodStr := cfg.Ini.Section("mailbox").Key("registration_period").MustString("8760h")
- registrationPeriod, err := time.ParseDuration(registrationPeriodStr)
+ monthlyFee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("monthly_fee").MustString("KUDOS:0"))
if err != nil {
- fmt.Printf("Failed to parse duration: %v", err)
+ fmt.Printf("Failed to parse cost: %v", err)
os.Exit(1)
}
- m.RegistrationPeriod = registrationPeriod
- fee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("registration_fee").MustString("KUDOS:0"))
+ m.MonthlyFee = monthlyFee
+ updateFee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("registration_update_fee").MustString("KUDOS:0"))
if err != nil {
fmt.Printf("Failed to parse cost: %v", err)
os.Exit(1)
}
- m.RegistrationFee = fee
+ m.RegistrationUpdateFee = updateFee
_db, err := gorm.Open(cfg.Db, &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
@@ -637,20 +717,44 @@ func (m *Mailbox) Initialize(cfg MailboxConfig) {
if err := m.Db.AutoMigrate(&InboxEntry{}); err != nil {
panic(err)
}
- if err := m.Db.AutoMigrate(&MailboxMessageKeys{}); err != nil {
+ if err := m.Db.AutoMigrate(&MailboxMetadata{}); err != nil {
+ panic(err)
+ }
+ go func() {
+ for {
+ tx := m.Db.Where("expiration < ?", time.Now()).Delete(&MailboxMetadata{})
+ m.Logf(LogInfo, "Cleaned up %d stale registrations.\n", tx.RowsAffected)
+ time.Sleep(time.Hour * 24)
+ }
+ }()
+ if err := m.Db.AutoMigrate(&PendingMailboxRegistration{}); err != nil {
panic(err)
}
+ // Clean up pending
+ pendingExpStr := cfg.Ini.Section("mailbox").Key("pending_registration_expiration").MustString("24h")
+ pendingExp, err := time.ParseDuration(pendingExpStr)
+ if err != nil {
+ fmt.Printf("Failed to parse pending registraion expiration: %v", err)
+ os.Exit(1)
+ }
+ go func() {
+ for {
+ tx := m.Db.Where("created_at < ?", time.Now().Add(-pendingExp)).Delete(&PendingMailboxRegistration{})
+ m.Logf(LogInfo, "Cleaned up %d stale pending registrations.\n", tx.RowsAffected)
+ time.Sleep(pendingExp)
+ }
+ }()
m.Merchant = cfg.Merchant
- if !fee.IsZero() {
+ if !monthlyFee.IsZero() {
merchConfig, err := m.Merchant.GetConfig()
if err != nil {
fmt.Printf("Failed to get merchant config: %v", err)
os.Exit(1)
}
- currencySpec, currencySupported := merchConfig.Currencies[fee.Currency]
+ currencySpec, currencySupported := merchConfig.Currencies[monthlyFee.Currency]
for !currencySupported {
- fmt.Printf("Currency `%s' not supported by merchant!\n", fee.Currency)
+ fmt.Printf("Currency `%s' not supported by merchant!\n", monthlyFee.Currency)
os.Exit(1)
}
m.CurrencySpec = currencySpec