v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s
Build and Push Docker Image / build (push) Successful in 5m1s
This commit is contained in:
@@ -4,11 +4,14 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/metazla/meta-core/internal/claim"
|
||||
"github.com/metazla/meta-core/internal/identity"
|
||||
"github.com/metazla/meta-core/internal/storage"
|
||||
)
|
||||
@@ -120,6 +123,9 @@ type identityDeleteResponse struct {
|
||||
|
||||
type identitySignRequest struct {
|
||||
BytesB64 string `json:"bytesB64"`
|
||||
// Format is "der" or "compact" (64-byte r||s, low-S). Default: compact for
|
||||
// MMUR2 claim payloads ("MMUR2\n" prefix), DER otherwise (MMUR1 back-compat).
|
||||
Format string `json:"format,omitempty"`
|
||||
// UID selects which account signs. Optional — when omitted and exactly one
|
||||
// account exists, that account is used (single-identity back-compat).
|
||||
UID string `json:"uid,omitempty"`
|
||||
@@ -143,6 +149,8 @@ type identitySignBatchRequest struct {
|
||||
// UID selects which account signs — same optional single-identity
|
||||
// back-compat as identitySignRequest.
|
||||
UID string `json:"uid,omitempty"`
|
||||
// Format: see identitySignRequest.Format; applies to every payload.
|
||||
Format string `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
type identitySignBatchResponse struct {
|
||||
@@ -445,10 +453,10 @@ func (s *Server) handleIdentityDelete(w http.ResponseWriter, r *http.Request) {
|
||||
"storage is not connected; refusing to delete the key while its data cannot be purged", true)
|
||||
return
|
||||
}
|
||||
purged, err := s.storage.UDLPurgeUser(id.UID)
|
||||
purged, err := s.retractAllClaims(r, id)
|
||||
if err != nil {
|
||||
writeErrorSlug(w, http.StatusInternalServerError, "purge_failed",
|
||||
"could not purge this account's data; the account was NOT deleted: "+err.Error(), true)
|
||||
"could not retract this account's claims; the account was NOT deleted: "+err.Error(), true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -456,7 +464,7 @@ func (s *Server) handleIdentityDelete(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorSlug(w, http.StatusInternalServerError, "delete_failed", err.Error(), false)
|
||||
return
|
||||
}
|
||||
log.Printf("[API] Deleted account %s and purged %d User Data Layer records across %d cids",
|
||||
log.Printf("[API] Deleted account %s after retracting %d claims across %d cids",
|
||||
id.UID, purged.Records, purged.Cids)
|
||||
writeJSON(w, http.StatusOK, identityDeleteResponse{UID: id.UID, Purged: purged})
|
||||
}
|
||||
@@ -485,9 +493,9 @@ func (s *Server) handleIdentitySign(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorSlug(w, http.StatusForbidden, "reserved_payload", reservedPayloadMessage, false)
|
||||
return
|
||||
}
|
||||
sig, err := identity.Sign(id, raw)
|
||||
sig, err := signPayload(id, raw, body.Format)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, identitySignResponse{
|
||||
@@ -537,9 +545,9 @@ func (s *Server) handleIdentitySignBatch(w http.ResponseWriter, r *http.Request)
|
||||
"payloadsB64["+strconv.Itoa(i)+"]: "+reservedPayloadMessage, false)
|
||||
return
|
||||
}
|
||||
sig, err := identity.Sign(id, raw)
|
||||
sig, err := signPayload(id, raw, body.Format)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
writeError(w, http.StatusBadRequest, "payloadsB64["+strconv.Itoa(i)+"]: "+err.Error())
|
||||
return
|
||||
}
|
||||
sigs = append(sigs, base64.StdEncoding.EncodeToString(sig))
|
||||
@@ -562,3 +570,61 @@ func (s *Server) handleIdentityAEADKey(w http.ResponseWriter, r *http.Request) {
|
||||
AEADKeyB64: base64.StdEncoding.EncodeToString(key),
|
||||
})
|
||||
}
|
||||
|
||||
// signPayload signs raw with the account key in the requested encoding.
|
||||
func signPayload(id *identity.Identity, raw []byte, format string) ([]byte, error) {
|
||||
switch format {
|
||||
case "compact":
|
||||
return identity.SignCompact(id, raw)
|
||||
case "der":
|
||||
return identity.Sign(id, raw)
|
||||
case "":
|
||||
if strings.HasPrefix(string(raw), claim.Domain) {
|
||||
return identity.SignCompact(id, raw)
|
||||
}
|
||||
return identity.Sign(id, raw)
|
||||
}
|
||||
return nil, fmt.Errorf("format must be \"compact\" or \"der\"")
|
||||
}
|
||||
|
||||
// retractAllClaims signs a retraction for every live claim the account holds
|
||||
// and stores them (G11): the tombstones stay, so the votes cannot be replayed.
|
||||
func (s *Server) retractAllClaims(r *http.Request, id *identity.Identity) (storage.UDLUserStats, error) {
|
||||
refs, err := s.storage.LiveClaimsBySigner(id.UID)
|
||||
if err != nil {
|
||||
return storage.UDLUserStats{}, err
|
||||
}
|
||||
cids, keys := map[string]bool{}, map[string]bool{}
|
||||
now := uint64(time.Now().UnixMilli())
|
||||
batch := make([]claim.Claim, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
cur, ok, err := s.storage.UDLGetClaim(id.UID, ref[0], ref[1])
|
||||
if err != nil {
|
||||
return storage.UDLUserStats{}, err
|
||||
}
|
||||
ts := now
|
||||
if ok && cur.TS >= ts {
|
||||
ts = cur.TS + 1
|
||||
}
|
||||
tomb := claim.Claim{CID: ref[0], UID: id.UID, Key: ref[1], Value: claim.Null(), TS: ts}
|
||||
if err := tomb.Sign(id); err != nil {
|
||||
return storage.UDLUserStats{}, err
|
||||
}
|
||||
batch = append(batch, tomb)
|
||||
cids[ref[0]], keys[ref[1]] = true, true
|
||||
}
|
||||
for start := 0; start < len(batch); start += storage.MaxClaimsPerBatch {
|
||||
end := min(start+storage.MaxClaimsPerBatch, len(batch))
|
||||
results, err := s.storage.WriteClaims(r.Context(), batch[start:end])
|
||||
if err != nil {
|
||||
return storage.UDLUserStats{}, err
|
||||
}
|
||||
for i, res := range results {
|
||||
if res.Status != storage.StatusAccepted && res.Status != storage.StatusUnchanged {
|
||||
return storage.UDLUserStats{}, fmt.Errorf("retraction of %s/%s refused: %s %s",
|
||||
batch[start+i].CID, batch[start+i].Key, res.Status, res.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
return storage.UDLUserStats{Records: len(batch), Cids: len(cids), Keys: len(keys)}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user