Subject: bug#73166: shell-autorized-directories

From: Nicolas Graves <[email-masked]>
Date: Sat, 09 Nov 2024 15:12:44 +0100

On 2024-09-11 16:11, Nicolas Graves wrote:

>> That option would add a line to ‘shell-autorized-directories’?
>
> Yes. Actually I would like to develop a little more after thinking about
> that.
>
> Let's say you git pull code from a guix-shell-authorized repo and the 
> pull includes some potentially harmful / dangerous code.
>
> The assumption of direnv is that the user has to allow the code to run
> again in this case, putting more emphasis on security. This is not the
> case in Guix, IIRC. I think it should be done in Guix too. 
>
> Implementing that kind of additional security will indeed need such an
> option, for this will need to actually include the hash of the file of
> something like that.
>
> It's actually quite simple in direnv, they take a sha256 hash of the
> absolute filename + the content of the file.
> (See
> https://github.com/nicolas-graves/python-direnv/blob/f8f0967a9772f0775ffe75a68d868c75076f5af4/direnv.py#L36)
> That hash makes a simple file-based database where a file is allowed based
> not only on its location but on its location+content.
>
> We could have two options to interact with such a database :
> --allow
> --revoke

Here's a working draft for some code for that.  This is currently able
to properly allow or deny my direnv-validated directories.  With a
proper direnv rename, we can almost already replace
authorized-shell-directory? function.

I feel like this is a far more secure and convenient way to manage
autorized-directories for guix shell.  WDYT ?

@Andrew you might also be interested in that given your focus on
per-directory complete dev environments.  Originally I thought of this
while thinking about how unsecure patch 3 of 
https://lists.sr.ht/~abcdw/rde-devel/patches/54944 was.

I'll probably continue to work on that to bring it to a full reviewable
patch, some input would be greatly appreciated in the meantime!

(use-modules
 (gcrypt hash)
 (guix rpm) ; for bytevector->hex-string
 (guix serialization)
 (srfi srfi-1)
 (srfi srfi-11)
 (srfi srfi-26)
 (srfi srfi-71)
 (ice-9 match)
 (ice-9 textual-ports))

(define (direnv-file-hash path)
  (let* ((abs-path (canonicalize-path path))
         (content (call-with-input-file abs-path get-string-all)))
    (call-with-input-string (string-append abs-path "\n" content)
      (cut (compose bytevector->hex-string port-sha256) <>))))

(define (xdg-data-home)
  (or (getenv "XDG_DATA_HOME")
      (string-append (getenv "HOME") "/.local/share")))

(define (permissions path)
  (define (is-valid? file-path)
    (and (file-exists? file-path)
         (string=? (string-trim-right
                    (call-with-input-file file-path get-string-all))
                   (canonicalize-path path))))

  (let* ((file-hash (direnv-file-hash path))
         (database-path (string-append (xdg-data-home) "/direnv/")))
    (cond
     ((is-valid? (string-append database-path "deny/" file-hash))
      (values 'deny file-hash))
     ((is-valid? (string-append database-path "allow/" file-hash))
      (values 'allow file-hash))
     (else
      (values 'unknown file-hash)))))

(define (is-allowed? path)
  (eq? 'allow (permissions path)))

(define (is-denied? path)
  (eq? 'deny (permissions path)))

(define (allow-or-deny! path target-type origin-type)
  (let ((type file-hash (permissions path))
        (data-home (string-append (xdg-data-home) "/direnv/")))
    (match type
      (origin-type
       (rename-file
        (string-append data-home (symbol->string origin-type) "/" file-hash)
        (string-append data-home (symbol->string target-type) "/" file-hash)))
      (target-type
       (warn "not necessary"))  ;TODO do that properly
      ('unknown
       (call-with-output-file
           (string-append data-home (symbol->string target-type) "/" file-hash)
         (cut display (canonicalize-path path) <>))))))

(define (allow! path)
  (allow-or-deny! path 'allow 'deny))

(define (deny! path)
  (allow-or-deny! path 'deny 'allow))

-- 
Best regards,
Nicolas Graves