Functional HTTP signatures

… using extenal binary of OpenSSL. Not ideal, but
it works for now!
This commit is contained in:
Jaidyn Ann 2024-06-06 23:53:43 -05:00
parent b66c166aff
commit 7adda41c71

View File

@ -1,4 +1,4 @@
;;; Copyright © 2023 Jaidyn Levesque <jadedctrl@posteo.at> ;;; Copyright © 2023-2024 Jaidyn Levesque <jadedctrl@posteo.at>
;;; ;;;
;;; This program is free software: you can redistribute it and/or ;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License ;;; modify it under the terms of the GNU Affero General Public License
@ -22,7 +22,7 @@
(defun users () (defun users ()
"List of the server's usernames." "List of the server's usernames."
'("servisto")) '("servistchjo"))
(defun userhosts () (defun userhosts ()
@ -38,6 +38,10 @@
(".well-known/host-meta" . http-host-meta))) (".well-known/host-meta" . http-host-meta)))
(defvar *privkey* (alexandria:read-file-into-string #p"enc/privkey.pem"))
(defvar *pubkey* (alexandria:read-file-into-string #p"enc/pubkey.pem"))
;; ———————————————————————————————————————— ;; ————————————————————————————————————————
;; Host-info response ;; Host-info response
@ -126,7 +130,7 @@ the plist in the docstring for its WEBTENTACLE:SERVER function."
(defun http-user-dir (env path-items params) (defun http-user-dir (env path-items params)
(let ((user (car path-items))) (let ((user (car path-items)))
;; In case of request for the user's actor. ;; In case of request for the user's actor.
(if (member user (users) :test 'string=) (if (member user (users) :test 'equal)
`(200 (:content-type "application/activity+json") `(200 (:content-type "application/activity+json")
(,(user-actor env user)))))) (,(user-actor env user))))))
@ -139,12 +143,15 @@ the plist in the docstring for its WEBTENTACLE:SERVER function."
(yason:encode-alist (yason:encode-alist
`(("@context" . ("https://www.w3.org/ns/activitystreams" `(("@context" . ("https://www.w3.org/ns/activitystreams"
"https://litepub.social/litepub/context.jsonld")) "https://litepub.social/litepub/context.jsonld"))
("endpoints" . ,(alexandria:plist-hash-table (list "sharedInbox" "https://etc.xwx.moe/inbox")))
("url" . ,user-root)
("id" . ,user-root) ("id" . ,user-root)
("type" . "Person") ("type" . "Person")
("preferredUsername" . ,username) ("preferredUsername" . ,username)
("name" . "Servistiĉo") ("name" . "Servistiĉo")
("inbox" . ,(str:concat user-root "/inbox.json")) ("inbox" . ,(str:concat user-root "/inbox"))
("outbox" . ,(str:concat user-root "/outbox.json")) ("outbox" . ,(str:concat user-root "/outbox"))
("discoverable" . t)
("summary" . "Mi estas simpla roboto, kiu montras ke iomete ekfunkcias activitypub-servist. ("summary" . "Mi estas simpla roboto, kiu montras ke iomete ekfunkcias activitypub-servist.
ĉu mi rajtas demeti la servistinan kostumon, nun? ĉu mi rajtas demeti la servistinan kostumon, nun?
Mi ne estas knabino!!") Mi ne estas knabino!!")
@ -152,7 +159,7 @@ Mi ne estas knabino!!")
. ,(alexandria:plist-hash-table . ,(alexandria:plist-hash-table
(list (list
"type" "Image" "type" "Image"
"url" "https://xwx.moe/etc/servisticho-profilbildo.png"))) "url" "https://xwx.moe/etc/servisticho-profilbildo.jpg")))
("image" ("image"
. ,(alexandria:plist-hash-table . ,(alexandria:plist-hash-table
(list (list
@ -196,27 +203,29 @@ Mi ne estas knabino!!")
(:hour 2) #\: (:min 2) #\: (:sec 2) " " :timezone)) (:hour 2) #\: (:min 2) #\: (:sec 2) " " :timezone))
(defun note-headers (inbox from to text) (defun note-headers (inbox from to json)
(let* ((json (note-json from to text)) (let* ((inbox-uri (quri:uri inbox))
(inbox-uri (quri:uri inbox))
(digest-header (str:concat "SHA-256=" (string-sha256sum json))) (digest-header (str:concat "SHA-256=" (string-sha256sum json)))
(date-header (date-header
(let ((local-time:*default-timezone* local-time:+gmt-zone+)) (let ((local-time:*default-timezone* local-time:+gmt-zone+))
(local-time:format-timestring (local-time:format-timestring
nil (local-time:now) nil (local-time:now)
:format +date-header-datetime-format+))) :format +date-header-datetime-format+)))
(signed-headers (str:unlines (signed-headers
(list (concatenate
(str:concat "(request target): post " (quri:uri-path inbox-uri)) 'string
(str:concat "host: " (quri:uri-host inbox-uri)) (format nil "(request-target): post ~A~%" (quri:uri-path inbox-uri))
(str:concat "date: " date-header) (format nil "host: ~A~%" (quri:uri-host inbox-uri))
(str:concat "digest: " digest-header)))) (format nil "date: ~A~%" date-header)
(format nil "digest: ~A" digest-header)))
(signature (base64:usb8-array-to-base64-string (signature (base64:usb8-array-to-base64-string
(ironclad:sign-message (openssl-shell-import-key-pair *privkey*) (ironclad:sign-message (openssl-shell-import-key-pair *privkey*)
(string-to-ub8-vector signed-headers)))) (string-to-ub8-vector
(string-sha256sum signed-headers)))))
(signature (openssl-shell-sign-string *privkey* signed-headers))
(signature-header (str:concat "keyId=\"" from "#main-key\"," (signature-header (str:concat "keyId=\"" from "#main-key\","
"headers=\"(request-target) host date digest\","
"algorithm=\"rsa-sha256\"," "algorithm=\"rsa-sha256\","
"headers=\"(request-target) host date digest\","
"signature=\"" signature "\""))) "signature=\"" signature "\"")))
`(("Date" . ,date-header) `(("Date" . ,date-header)
("Digest" . ,digest-header) ("Digest" . ,digest-header)
@ -228,8 +237,10 @@ Mi ne estas knabino!!")
(defun send-note (inbox from to text) (defun send-note (inbox from to text)
(dexador:post inbox :content (note-json from to text) (let* ((json (note-json from to text))
:headers (note-headers inbox from to text))) (headers (note-headers inbox from to json)))
(dexador:post inbox :content json
:headers headers)))
@ -249,7 +260,7 @@ Mi ne estas knabino!!")
;; ———————————————————————————————————————— ;; ————————————————————————————————————————
(defun server (env) (defun server (env)
"Returns the response data for Clack, given the request data `env`." "Returns the response data for Clack, given the request data `env`."
(setq *logs* (append *logs* (list env))) (setq *logs* (append *logs* (list env (babel:octets-to-string (alexandria:read-stream-content-into-byte-vector (getf env :raw-body))))))
(let* ((path (pathname-sans-parameters (getf env :request-uri))) (let* ((path (pathname-sans-parameters (getf env :request-uri)))
(params (pathname-parameters (getf env :request-uri))) (params (pathname-parameters (getf env :request-uri)))
(response-function (response-function
@ -264,7 +275,7 @@ Mi ne estas knabino!!")
(funcall 'http-404 env path-sans-response-root params)))) (funcall 'http-404 env path-sans-response-root params))))
(defun start-server (&optional (config '(:domain "etc.xwx.moe"))) (defun start-server (&optional (config '(:domain "localhost" :port 8080)))
"Start the server." "Start the server."
(clack:clackup (lambda (env) (clack:clackup (lambda (env)
(server (append env config))) (server (append env config)))
@ -457,3 +468,16 @@ returned values: An Ironclad private key, and an Ironclad public key."
:rsa :rsa
:n (getf key-values :n) :n (getf key-values :n)
:e (getf key-values :e))))) :e (getf key-values :e)))))
(defun openssl-shell-sign-string (private-pem-string string)
"Use the OpenSSL binary on the host system to RSS-SHA256 sign a STRING with a
private key."
(alexandria:write-string-into-file private-pem-string #p"private.pem" :if-exists :overwrite)
(apply #'str:concat
(inferior-shell:run/lines
`(inferior-shell:pipe
(printf ,string)
(openssl dgst -sha256 -sign private.pem -)
(base64)))))