-
-
Notifications
You must be signed in to change notification settings - Fork 6
Description
I’m currently working on a script that runs ssh-keygen. When I try to resholve the script that I’m working on, I get this error:
'ssh-keygen' _might_ be able to execute its arguments, and I don't have any command-specific rules for figuring out if this specific invocation does or not.
When I encounter an error like that one, I usually do the following:
-
Read through the command’s documentation to see if there are any arguments that should be resolved.
-
If there are any arguments that should be resolved, then open a resholve pull request that adds a parser to resholve.
-
If there aren’t any arguments that should be resolved, then open a Nixpkgs pull request that adds a lore override.
I tried following that process with ssh-keygen, but I ran into a problem. ssh-keygen has an option named force-command. It’s not clear to me whether the command for force-command should be resolved or not. Here’s some example code that uses force-command:
flake.nix:
# This file is dedicated to the public domain using 🅭🄍1.0:
# <https://creativecommons.org/publicdomain/zero/1.0>.
{
inputs = {
resholve.url = "github:abathur/resholve";
nixpkgs.follows = "resholve/nixpkgs";
};
outputs =
{
resholve,
nixpkgs,
self,
}:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages."${system}";
in
{
formatter."${system}" = pkgs.nixfmt-rfc-style;
packages."${system}".default =
let
inherit (nixpkgs.lib.strings) escapeShellArg;
name = "ssh-keygen-example";
interpreter = nixpkgs.lib.meta.getExe pkgs.bash;
dependencies = [
pkgs.coreutils
pkgs.openssh
];
execerWorkaround = "cannot:${pkgs.openssh}/bin/ssh-keygen";
pathList = nixpkgs.lib.strings.makeBinPath dependencies;
lore = pkgs.binlore.collect { drvs = dependencies; };
script = ''
set -o errexit -o nounset -o pipefail
readonly tempdir="$(mktemp --directory --suffix=-ssh-keygen-example)"
trap 'cd /; rm --recursive --force "$tempdir"' EXIT
cd "$tempdir"
echo Generating a CA key…
ssh-keygen -f ./ca-key -N ""
echo Done.
echo
echo Generating a user key…
ssh-keygen -f ./user-key -N ""
echo Done.
echo
echo Generating a certificate…
ssh-keygen -s ./ca-key -I my-identity -O force-command=hello ./user-key.pub
echo Done.
echo
echo Raw certificate:
cat ./user-key-cert.pub
echo
echo Decoded certificate:
ssh-keygen -Lf ./user-key-cert.pub
'';
in
pkgs.runCommandWith
{
inherit name;
derivationArgs.nativeBuildInputs = [ resholve.packages."${system}".default ];
}
''
mkdir --parents "$out/bin"
readonly script_path="$out/bin/"${escapeShellArg name}
printf '%s' ${escapeShellArg script} > "$script_path"
readonly resholve_args=(
--interpreter ${escapeShellArg interpreter}
--path ${escapeShellArg pathList}
--lore ${escapeShellArg lore}
# Uncomment this next line to work around this issue:
#--execer ${escapeShellArg execerWorkaround}
--overwrite
"$script_path"
)
resholve "''${resholve_args[@]}"
chmod +x "$script_path"
'';
};
}flake.nix
{
"nodes": {
"binlore": {
"inputs": {
"flake-compat": [
"resholve",
"flake-compat"
],
"flake-utils": [
"resholve",
"flake-utils"
],
"nixpkgs": [
"resholve",
"nixpkgs"
],
"yallback": "yallback"
},
"locked": {
"lastModified": 1711149136,
"narHash": "sha256-CQOGpmH8eG8GLxC8xfUcdYvVcAwmpIkxohw4dCWwTc4=",
"owner": "abathur",
"repo": "binlore",
"rev": "a425d4a1d06ab8d952f49d61fea292ede53dd25c",
"type": "github"
},
"original": {
"owner": "abathur",
"repo": "binlore",
"type": "github"
}
},
"d-mark-python": {
"inputs": {
"flake-compat": [
"resholve",
"wwurst",
"flake-compat"
],
"flake-utils": [
"resholve",
"wwurst",
"flake-utils"
],
"nixpkgs": [
"resholve",
"wwurst",
"nixpkgs"
]
},
"locked": {
"lastModified": 1695101569,
"narHash": "sha256-NvqGEy9lbTX5lzvGEiJtwO97DGOd33yrBPdu2SowkZg=",
"owner": "abathur",
"repo": "d-mark-python",
"rev": "da6d97b06333e7e8c6bd1f513b985e0e4649f73a",
"type": "github"
},
"original": {
"owner": "abathur",
"ref": "draft",
"repo": "d-mark-python",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1713687659,
"narHash": "sha256-Yd8KuOBpZ0Slau/NxFhMPJI0gBxeax0vq/FD0rqKwuQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f2d7a289c5a5ece8521dd082b81ac7e4a57c2c5c",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"resholve": {
"inputs": {
"binlore": "binlore",
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"wwurst": "wwurst"
},
"locked": {
"lastModified": 1716380986,
"narHash": "sha256-D8HKfam0cuaI/YxRGHdyq9RWnsymh44GCaltK5nmUVc=",
"owner": "abathur",
"repo": "resholve",
"rev": "ebd7901b36a0830b67cf300e2ee5719b867a2059",
"type": "github"
},
"original": {
"owner": "abathur",
"repo": "resholve",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": [
"resholve",
"nixpkgs"
],
"resholve": "resholve"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"wwurst": {
"inputs": {
"d-mark-python": "d-mark-python",
"flake-compat": [
"resholve",
"flake-compat"
],
"flake-utils": [
"resholve",
"flake-utils"
],
"nixpkgs": [
"resholve",
"nixpkgs"
]
},
"locked": {
"lastModified": 1695101647,
"narHash": "sha256-fzloQiSPB0nAeQP/71avDZTHFIVnHw8BJPiZ3T24hCA=",
"owner": "abathur",
"repo": "wordswurst",
"rev": "61a0ee22bbb7bea8d95568c0a43530fb4671139b",
"type": "github"
},
"original": {
"owner": "abathur",
"repo": "wordswurst",
"type": "github"
}
},
"yallback": {
"inputs": {
"flake-compat": [
"resholve",
"binlore",
"flake-compat"
],
"flake-utils": [
"resholve",
"binlore",
"flake-utils"
],
"nixpkgs": [
"resholve",
"binlore",
"nixpkgs"
]
},
"locked": {
"lastModified": 1695100875,
"narHash": "sha256-4/IE4Hoh60vBAM34nEDgRiDpSSGuqCMUZmm88PA0q8k=",
"owner": "abathur",
"repo": "yallback",
"rev": "e98c84ef60477035af7bb36177fd393f20280ae4",
"type": "github"
},
"original": {
"owner": "abathur",
"repo": "yallback",
"type": "github"
}
}
},
"root": "root",
"version": 7
}If you try to run that code as is, then it will give this error:
$ nix --extra-experimental-features 'nix-command flakes' run .
error: builder for '/nix/store/dsgmjg3mdbbcrr73d0mrp1wydina4qiz-ssh-keygen-example.drv' failed with exit code 9;
last 3 log lines:
> ssh-keygen -f ./ca-key -N ""
> ^~~~~~~~~~
> /nix/store/2xwpi4009g43liphwbimmqns2hyis0wc-ssh-keygen-example/bin/ssh-keygen-example:8: 'ssh-keygen' _might_ be able to execute its arguments, and I don't have any command-specific rules for figuring out if this specific invocation does or not.
For full logs, run:
nix-store -l /nix/store/dsgmjg3mdbbcrr73d0mrp1wydina4qiz-ssh-keygen-example.drv
$
If you uncomment the line for the workaround, then it will print the following:
$ nix --extra-experimental-features 'nix-command flakes' run .
Generating a CA key…
Generating public/private ed25519 key pair.
Your identification has been saved in ./ca-key
Your public key has been saved in ./ca-key.pub
The key fingerprint is:
SHA256:6daW6xa7eZ3g+AQ5bq41hBhMD+tQLhLUyuX/bTI0f0w jayman@Jason-Desktop-Linux
The key's randomart image is:
+--[ED25519 256]--+
| .o. + |
| .o= + |
| ..+o = . |
| o..+ o o . |
| .o S = |
| ..o+.+E |
| oo+OB.o . |
| .+=B== o |
| .B*=. |
+----[SHA256]-----+
Done.
Generating a user key…
Generating public/private ed25519 key pair.
Your identification has been saved in ./user-key
Your public key has been saved in ./user-key.pub
The key fingerprint is:
SHA256:hGOiBsau+eDCWA7ijLmohM6lP5Fbs7hhW2r3+1HKpJM jayman@Jason-Desktop-Linux
The key's randomart image is:
+--[ED25519 256]--+
| |
|. . |
|.o . + . |
|o. . o o |
| .o . S. . |
|=o.o o = o |
|%* +=.oE + |
|@*===o . . |
|**+=+ .oo. |
+----[SHA256]-----+
Done.
Generating a certificate…
Signed user key ./user-key-cert.pub: id "my-identity" serial 0 valid forever
Done.
Raw certificate:
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAII83C/9GhwO+MGwisav8smmS8CO+8UpljhwC3mneM8QxAAAAIOzawETSeHXg4u1H+JMJkTZfvuFTDPRn8RtvMIPrroAhAAAAAAAAAAAAAAABAAAAC215LWlkZW50aXR5AAAAAAAAAAAAAAAA//////////8AAAAeAAAADWZvcmNlLWNvbW1hbmQAAAAJAAAABWhlbGxvAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAxPF6nX+0O0zTaSvGwJNHQfIsxkVyDrfB6XfdId0PwhAAAAFMAAAALc3NoLWVkMjU1MTkAAABAxPHjU6J1IavExf58fCOjZtbjhMcA2RwPgOqZB0WZPichuH/J6UNg4Wmza1YgStQWN4RXhPkmRosW4mIciVsdDw== jayman@Jason-Desktop-Linux
Decoded certificate:
./user-key-cert.pub:
Type: ssh-ed25519-cert-v01@openssh.com user certificate
Public key: ED25519-CERT SHA256:hGOiBsau+eDCWA7ijLmohM6lP5Fbs7hhW2r3+1HKpJM
Signing CA: ED25519 SHA256:6daW6xa7eZ3g+AQ5bq41hBhMD+tQLhLUyuX/bTI0f0w (using ssh-ed25519)
Key ID: "my-identity"
Serial: 0
Valid: forever
Principals: (none)
Critical Options:
force-command hello
Extensions:
permit-X11-forwarding
permit-agent-forwarding
permit-port-forwarding
permit-pty
permit-user-rc
$
I’m not sure whether or not resholve should resolve the command for force-command. force-command is an unusual case for resholve.
Normally, arguments are resolved in order to make sure that scripts don’t have undeclared or unpinned dependencies. In this case, force-command doesn’t necessarily add any additional dependencies to the script. You could replace force-command=hello with force-command=/var/empty/command-that-doesn’t-exist in the above script, and it wouldn’t cause any problems. On the other hand, you could theoretically write a script that first generates a certificate, decodes it, and then executes the force-command command. For that script, it would make more sense to resolve the force-command command.
Additionally, a user might use a resholved script to automatically generate an SSH certificate as a part of a NixOS configuration. Normally, using Nix store paths in generated files is a good thing because it makes the generated file depend on the store paths contained in the generated file. In this case though, the generated file doesn’t actually contain the literal store path so there won’t be a dependency. In this situation, it might be more reliable to use an unresolved command (e.g., force-command=hello instead of force-command=/nix/store/lz9gfg6iybsh0hiignpk55w99a3bj4vb-hello-2.12.1/bin/hello). Users would have to make sure that the package is in environment.systemPackages (or whatever), but they would know for a fact that it would be present. In this situation, it wouldn’t necessarily be guaranteed that the Nix store path for a particular version of the command would be present.
Finally, there might be security problems if we resolve force-command commands. For example, let’s say that resholve transforms force-command=rush into force-command=/nix/store/xid0zaqlz5ir8805yviggsrwiis6qljn-rush-2.4/bin/rush. The script gets run and user certificates are generated that have force-command set to /nix/store/xid0zaqlz5ir8805yviggsrwiis6qljn-rush-2.4/bin/rush. Later, a security vulnerability is discovered in GNU Rush 2.4. Version 2.5 is released to fix the security vulnerability. In the first scenario that I can think of, the systems administrator updates to GNU Rush 2.5 but doesn’t realize that they need to rebuild and rerun the script. As a result, there are still user certificates that are in use that use the old vulnerable version of GNU Rush. An attacker uses one of the certificates and exploits the old vulnerable version of GNU Rush. In the second scenario that I can think of, the systems administrator updates to GNU Rush 2.5 and generates new user certificates with force-command set to /nix/store/9abxgjk1dmy9wa9ls9hfm6lmgkbk8skp-rush-2.5/bin/rush. Unfortunately, the older user certificates are still valid. An attacker uses one of the older user certificates and exploits the old vulnerable version of GNU Rush.
I don’t really know if any of the potential situations that I just listed are realistic because I don’t really understand SSH certificates (this is the first that I’ve ever heard of them). I just know that it’s not clear to me whether or not resholve should do anything if a script contains ssh-keygen -O force-command=<whatever>.