Ensure the signature’s domain matches the signee’s

A signature-key with a different omain from the
actor of the activity or the actiivty’s ID itself
might be indicative of tampering; disallow it.
This commit is contained in:
Jaidyn Ann 2025-01-09 22:27:19 -06:00
parent 7b3cf9cb89
commit 7f5b6ffc57
Signed by: jadedctrl
GPG Key ID: FEF4FCF78B4BF019

View File

@ -143,7 +143,7 @@ Returns the object if it was retrieved or fetched; nil otherwise."
;;; Signature HTTP-header parsing ;;; Signature HTTP-header parsing
;;; ———————————————————————————————————————— ;;; ————————————————————————————————————————
(defun signature-valid-p (env &key (current-time (get-universal-time))) (defun signature-valid-p (env activity &key (current-time (get-universal-time)))
"Return whether or not the Clack HTTP-request ENVs signature is valid. "Return whether or not the Clack HTTP-request ENVs signature is valid.
Only RSA-SHA256 signatures are supported. Only RSA-SHA256 signatures are supported.
Might provide a condition detailing the reason of the signatures invalidity as Might provide a condition detailing the reason of the signatures invalidity as
@ -161,6 +161,8 @@ https://swicg.github.io/activitypub-http-signature/"
(signed-str (signed-string env signature-alist :current-time current-time))) (signed-str (signed-string env signature-alist :current-time current-time)))
(when (and algorithm (not (string-equal (cdr algorithm) "rsa-sha256"))) (when (and algorithm (not (string-equal (cdr algorithm) "rsa-sha256")))
(signal 'invalid-signature-algorithm :algorithm (cdr algorithm))) (signal 'invalid-signature-algorithm :algorithm (cdr algorithm)))
(when (not (matching-domains-p signature-alist activity))
(signal 'invalid-signature-domain-mismatch))
(list (list
(gethash "https://w3id.org/security#publicKeyPem" (signature-key signature-alist)) (gethash "https://w3id.org/security#publicKeyPem" (signature-key signature-alist))
signed-str signed-str
@ -168,6 +170,27 @@ https://swicg.github.io/activitypub-http-signature/"
(invalid-signature (err) (invalid-signature (err)
(values nil err)))) (values nil err))))
(defun matching-domains-p (signature-alist activity)
"Returns whether or not the domain names within an ACTIVITY match, for ensuring
its signature is applicable: Those of the signature keys, the actors @ID,
and the ACTIVITYs @ID.
If these all match the same domain, and the signature is valid, we can safely say
the ACTIVITY did indeed come from that domain."
(labels ((uri-string (slot-value)
(typecase slot-value
(string slot-value)
(json-ld:object
(json-ld:@id slot-value))))
(domain-name (slot-value)
(let ((uri-string (uri-string slot-value)))
(when uri-string
(quri:uri-domain (quri:uri uri-string))))))
(equal*
(remove-if #'not
(list (domain-name activity)
(domain-name (as/v/a:actor activity))
(domain-name (assoc :keyid signature-alist)))))))
(defun signature-header-parse (signature-header) (defun signature-header-parse (signature-header)
"Parses the signature header into an associative list of the form: "Parses the signature header into an associative list of the form:
'((:KEYID . https://jam.xwx.moe/users/jadedctrl#main-key) '((:KEYID . https://jam.xwx.moe/users/jadedctrl#main-key)
@ -267,6 +290,13 @@ https://swicg.github.io/activitypub-http-signature/#how-to-obtain-a-signature-s-
(slot-value condition 'algorithm)))) (slot-value condition 'algorithm))))
(:documentation "Thrown during HTTP signature-validation, when the algorithm is unsupported.")) (:documentation "Thrown during HTTP signature-validation, when the algorithm is unsupported."))
(define-condition invalid-signature-domain-mismatch (invalid-signature)
()
(:report (lambda (condition stream)
(format stream "There is a domain-name mismatch within the activity, and so we cant say for sure the signature is valid.~%
Check the ID domain-names of the actor, the activity, and the signature-key.~&")))
(:documentation "Thrown during HTTP signature-validation, when it's noticed that domains-names for ID URIs don't match."))
;;; Fetching public keys ;;; Fetching public keys
@ -382,13 +412,14 @@ can be found). Uses the callback :RETRIEVE, defined in *CONFIG*."
(defun http-inbox (env path-items params) (defun http-inbox (env path-items params)
"If one tries to send an activity to our inbox, pass it along to "If one tries to send an activity to our inbox, pass it along to
the overloaded RECEIVE method." the overloaded RECEIVE method."
(let* ((contents (body-contents env))) (let* ((contents (body-contents env))
(json-contents (json-ld:parse contents)))
(multiple-value-bind (signature-valid-p signature-error) (multiple-value-bind (signature-valid-p signature-error)
(signature-valid-p env) (signature-valid-p env json-contents)
(cond (signature-error (signal signature-error)) (cond (signature-error (signal signature-error))
((not signature-valid-p) ((not signature-valid-p)
(signal 'http-result :status 401 :message "Failed to verify signature. Heck! TvT")) (signal 'http-result :status 401 :message "Failed to verify signature. Heck! TvT"))
((receive (json-ld:parse contents)) ((receive json-contents)
'(200 (:content-type "text/plain") ("You win!"))))))) '(200 (:content-type "text/plain") ("You win!")))))))
@ -568,3 +599,7 @@ or “/bear/apple/” or “/bear/”, but not “/bear” (not a directory)."
(loop for number across (loop for number across
sequence sequence
collect (format nil "~X" number)))) collect (format nil "~X" number))))
(defun equal* (&rest items)
"Whether or not all ITEMS are EQUAL to one another."
(loop for item in items
always (equal item (car items))))