Add deferred NIP-98 verification and use it for file chunks, room attachments and Git requests. Preserve body validation after reading each stream and cover deferred validation and replay handling with ordinary tests.
- commit
- d0a14b01e65dc0ca2eb9b4f8d2fff7f59add59c6
- parent
- e8d32f37a63d7a5388d2d0a4bc2d170bbdf13d58
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 2 | +- |
| M | internal/ | 6 | +++++- |
| M | protocol/ | 13 | ++++++++++++- |
| M | protocol/ | 23 | +++++++++++++++++++++++ |
@@ -243,7 +243,7 @@ func (t *Tenant) serveRoomMedia(w http.ResponseWriter, r *http.Request, hash str
243243 func (t *Tenant) uploadRoomAttachment(w http.ResponseWriter, r *http.Request, roomID string) {244244 // Authenticate and authorize before buffering file bytes. The payload245245 // binding is checked after reading, without consuming the proof twice.246- token, err := t.auth.VerifyNIP98(r.Header.Get("Authorization"), t.requestURL(r), r.Method, "")246+ token, err := t.auth.VerifyNIP98Deferred(r.Header.Get("Authorization"), t.requestURL(r), r.Method)247247 if err != nil {248248 browseHTTPError(w, err)249249 return@@ -372,6 +372,10 @@ func (t *Tenant) authorizeBlob(r *http.Request, action blob.Action) (string, err
372372 err = errors.New("auth-required: remote upload token payload must hash an empty body")373373 }374374 pubkey = token.PubKey375+ } else if action == blob.ActionUpload && (r.Method == http.MethodPut || r.Method == http.MethodPatch) {376+ var token event.Event377+ token, err = t.auth.VerifyNIP98Deferred(r.Header.Get("Authorization"), t.requestURL(r), r.Method)378+ pubkey = token.PubKey375379 } else {376380 var keys []string377381 keys, err = t.auth.WhoAsks(r.Header.Get("Authorization"), t.requestURL(r), r.Method, body, string(action), hash)@@ -547,7 +551,7 @@ func (t *Tenant) authorizeGit(ctx context.Context, r *http.Request, repo gitrela
547551 e, ok := privateGitProof(r)548552 var err error549553 if !ok {550- e, err = t.auth.VerifyNIP98(r.Header.Get("Authorization"), t.requestURL(r), r.Method, "")554+ e, err = t.auth.VerifyNIP98Deferred(r.Header.Get("Authorization"), t.requestURL(r), r.Method)551555 if err != nil {552556 return err553557 }@@ -38,6 +38,17 @@ func NewValidator(now func() time.Time) *Validator {
3838 // method and body supplied by the caller. Successfully verified event IDs are3939 // retained for the one-minute replay window.4040 func (v *Validator) VerifyNIP98(header, rawURL, method, body string) (nostr.Event, error) {41+ return v.verifyNIP98(header, rawURL, method, body, true)42+}43+44+// VerifyNIP98Deferred authenticates a proof before a streamed body is45+// read. Callers must check the payload tag against the resulting content hash46+// before accepting the body, such as with ValidateBlobPayload for uploads.47+func (v *Validator) VerifyNIP98Deferred(header, rawURL, method string) (nostr.Event, error) {48+ return v.verifyNIP98(header, rawURL, method, "", false)49+}50+51+func (v *Validator) verifyNIP98(header, rawURL, method, body string, checkPayload bool) (nostr.Event, error) {4152 e, err := decodeToken(header, "NIP-98")4253 if err != nil {4354 return nostr.Event{}, err@@ -58,7 +69,7 @@ func (v *Validator) VerifyNIP98(header, rawURL, method, body string) (nostr.Even
5869 if strings.ToUpper(nostr.Tag(e, "method")) != strings.ToUpper(method) {5970 return nostr.Event{}, authError("token was signed for another method")6071 }61- if body != "" && nostr.Tag(e, "payload") != hashBytes([]byte(body)) {72+ if checkPayload && body != "" && nostr.Tag(e, "payload") != hashBytes([]byte(body)) {6273 return nostr.Event{}, authError("token payload hash does not match the body")6374 }6475 if err := v.mark(e.ID, now); err != nil {@@ -27,6 +27,29 @@ func TestNIP98ValidatesRequestAndRejectsReplay(t *testing.T) {
2727 }2828 }2929 30+func TestNIP98DeferredAllowsPayloadUntilBodyIsMaterialized(t *testing.T) {31+ now := time.Unix(1_700_000_000, 0)32+ validator := NewValidator(func() time.Time { return now })33+ e := signedEvent(t, 27235, now.Unix(), "", [][]string{34+ {"u", "https://relay.example/api"},35+ {"method", "POST"},36+ {"payload", sha256Hex("body")},37+ })38+ header := token(t, e)39+ if _, err := validator.VerifyNIP98Deferred(header, "https://relay.example/api", "POST"); err != nil {40+ t.Fatalf("deferred verification: %v", err)41+ }42+ if err := validator.ValidateBlobPayload(header, sha256Hex("body")); err != nil {43+ t.Fatalf("validate streamed body: %v", err)44+ }45+ if err := validator.ValidateBlobPayload(header, sha256Hex("other")); err == nil {46+ t.Fatal("accepted a different streamed body")47+ }48+ if _, err := validator.VerifyNIP98Deferred(header, "https://relay.example/api", "POST"); err == nil || !strings.Contains(err.Error(), "replay") {49+ t.Fatalf("expected replay rejection, got %v", err)50+ }51+}52+3053 func TestNIP98ExpiryAndPayload(t *testing.T) {3154 now := time.Unix(1_700_000_000, 0)3255 validator := NewValidator(func() time.Time { return now })