├── .github ├── dependabot.yml └── workflows │ └── update-vendor-hash.yml ├── .gitignore ├── .mergify.yml ├── LICENSE ├── README.md ├── bin └── create-release.sh ├── convert.go ├── default.nix ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── lint.nix ├── main.go ├── main_test.go ├── renovate.json ├── scripts └── update-vendor-hash.sh ├── test-assets └── id_rsa └── treefmt.nix /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/update-vendor-hash.yml: -------------------------------------------------------------------------------- 1 | name: Update vendorHash 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | 7 | jobs: 8 | dependabot: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'mic92-renovate[bot]' }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | ref: ${{ github.event.pull_request.head.sha }} 15 | fetch-depth: 0 16 | - name: Install Nix 17 | uses: cachix/install-nix-action@v31 18 | with: 19 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 20 | nix_path: nixpkgs=channel:nixos-unstable 21 | - name: Update checksum 22 | run: | 23 | ./scripts/update-vendor-hash.sh 24 | # git push if we have a diff 25 | if [[ -n $(git diff) ]]; then 26 | git add default.nix 27 | git config --global user.email "<49699333+dependabot[bot]@users.noreply.github.com>" 28 | git config --global user.name "dependabot[bot]" 29 | git commit -m "update vendorHash" 30 | git push origin HEAD:${{ github.head_ref }} 31 | fi 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | /ssh-to-pgp 18 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | queue_conditions: 4 | - base=main 5 | - label~=merge-queue|dependencies 6 | merge_conditions: 7 | - check-success=buildbot/nix-build 8 | merge_method: rebase 9 | 10 | pull_request_rules: 11 | - name: refactored queue action rule 12 | conditions: [] 13 | actions: 14 | queue: 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jörg Thalheim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh-to-pgp 2 | 3 | Convert SSH RSA keys to GPG keys 4 | 5 | ## Usage 6 | 7 | - Exports the private: 8 | 9 | ```console 10 | $ ssh-to-pgp -private-key -i $HOME/.ssh/id_rsa -o private-key.asc 11 | 2504791468b153b8a3963cc97ba53d1919c5dfd4 12 | ``` 13 | 14 | - Exports the public key: 15 | 16 | ```console 17 | $ ssh-to-pgp -i $HOME/.ssh/id_rsa -o public-key.asc 18 | 2504791468b153b8a3963cc97ba53d1919c5dfd4 19 | ``` 20 | 21 | ## Install with nix 22 | 23 | ```console 24 | $ nix-shell -p 'import (fetchTarball "https://github.com/Mic92/ssh-to-pgp/archive/main.tar.gz") {}' 25 | ``` 26 | 27 | ## Install with go 28 | 29 | ```console 30 | $ go get github.com/Mic92/ssh-to-pgp 31 | ``` 32 | -------------------------------------------------------------------------------- /bin/create-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu -o pipefail 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 6 | cd $SCRIPT_DIR/.. 7 | 8 | version=${1:-} 9 | if [[ -z "$version" ]]; then 10 | echo "USAGE: $0 version" >&2 11 | exit 1 12 | fi 13 | 14 | if [[ "$(git symbolic-ref --short HEAD)" != "main" ]]; then 15 | echo "must be on main branch" >&2 16 | exit 1 17 | fi 18 | 19 | # ensure we are up-to-date 20 | uncommited_changes=$(git diff --compact-summary) 21 | if [[ -n "$uncommited_changes" ]]; then 22 | echo -e "There are uncommited changes, exiting:\n${uncommited_changes}" >&2 23 | exit 1 24 | fi 25 | git pull git@github.com:Mic92/ssh-to-pgp main 26 | unpushed_commits=$(git log --format=oneline origin/main..main) 27 | if [[ "$unpushed_commits" != "" ]]; then 28 | echo -e "\nThere are unpushed changes, exiting:\n$unpushed_commits" >&2 29 | exit 1 30 | fi 31 | sed -i -e "s!version = \".*\"!version = \"${version}\"!" default.nix 32 | git add default.nix 33 | nix-build default.nix 34 | git commit -m "bump version ${version}" 35 | git tag -e "${version}" 36 | 37 | echo 'now run `git push --tags origin main`' 38 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rsa" 6 | "fmt" 7 | "reflect" 8 | "time" 9 | 10 | "github.com/ProtonMail/go-crypto/openpgp" 11 | "github.com/ProtonMail/go-crypto/openpgp/packet" 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | func parsePrivateKey(sshPrivateKey []byte) (*rsa.PrivateKey, error) { 16 | privateKey, err := ssh.ParseRawPrivateKey(sshPrivateKey) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | rsaKey, ok := privateKey.(*rsa.PrivateKey) 22 | 23 | if !ok { 24 | return nil, fmt.Errorf("only RSA keys are supported right now, got: %s", reflect.TypeOf(privateKey)) 25 | } 26 | 27 | return rsaKey, nil 28 | } 29 | 30 | func SSHPrivateKeyToPGP(sshPrivateKey []byte, name string, comment string, email string) (*openpgp.Entity, error) { 31 | key, err := parsePrivateKey(sshPrivateKey) 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to parse private ssh key: %w", err) 34 | } 35 | 36 | // Let's make keys reproducible 37 | timeNull := time.Unix(0, 0) 38 | 39 | gpgKey := &openpgp.Entity{ 40 | PrimaryKey: packet.NewRSAPublicKey(timeNull, &key.PublicKey), 41 | PrivateKey: packet.NewRSAPrivateKey(timeNull, key), 42 | Identities: make(map[string]*openpgp.Identity), 43 | } 44 | uid := packet.NewUserId(name, comment, email) 45 | if uid == nil { 46 | return nil, fmt.Errorf("userid contains invalid characters (i.e. '\x00', '(', ')', '<' and '>'): name='%s' comment='%s' email='%s'", name, comment, email) 47 | } 48 | isPrimaryID := true 49 | selfSignature := &packet.Signature{ 50 | CreationTime: timeNull, 51 | SigType: packet.SigTypePositiveCert, 52 | PubKeyAlgo: packet.PubKeyAlgoRSA, 53 | Hash: crypto.SHA256, 54 | IsPrimaryId: &isPrimaryID, 55 | FlagsValid: true, 56 | FlagSign: true, 57 | FlagCertify: true, 58 | FlagEncryptStorage: true, 59 | FlagEncryptCommunications: true, 60 | IssuerKeyId: &gpgKey.PrimaryKey.KeyId, 61 | } 62 | gpgKey.Identities[uid.Id] = &openpgp.Identity{ 63 | Name: uid.Id, 64 | UserId: uid, 65 | SelfSignature: selfSignature, 66 | Signatures: []*packet.Signature{selfSignature}, 67 | } 68 | err = gpgKey.Identities[uid.Id].SelfSignature.SignUserId(uid.Id, gpgKey.PrimaryKey, gpgKey.PrivateKey, nil) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return gpgKey, nil 74 | } 75 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | vendorHash ? "sha256-XDd5w2FsjPTAtPYpcXuaVPeUnK/qemEC4Ku7tEw8rNg=", 4 | }: 5 | pkgs.buildGoModule { 6 | pname = "ssh-to-pgp"; 7 | version = "1.1.6"; 8 | 9 | src = ./.; 10 | 11 | inherit vendorHash; 12 | 13 | checkInputs = [ pkgs.gnupg ]; # no longer needed when we get rid of nixpkgs 22.11 14 | nativeCheckInputs = [ pkgs.gnupg ]; 15 | checkPhase = '' 16 | HOME=$TMPDIR go test . 17 | ''; 18 | 19 | shellHook = '' 20 | unset GOFLAGS 21 | ''; 22 | 23 | doCheck = true; 24 | 25 | meta = with pkgs.lib; { 26 | description = "Convert ssh private keys to PGP"; 27 | homepage = "https://github.com/Mic92/sops-nix"; 28 | license = licenses.mit; 29 | maintainers = with maintainers; [ mic92 ]; 30 | platforms = platforms.unix; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1748821116, 9 | "narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 315532800, 24 | "narHash": "sha256-zZGnqFOaEHRxiSAqTnBHQ+HNZCxMLumNzqtxRMhS4bk=", 25 | "rev": "5929de975bcf4c7c8d8b5ca65c8cd9ef9e44523e", 26 | "type": "tarball", 27 | "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre809520.5929de975bcf/nixexprs.tar.xz" 28 | }, 29 | "original": { 30 | "type": "tarball", 31 | "url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz" 32 | } 33 | }, 34 | "nixpkgs-lib": { 35 | "locked": { 36 | "lastModified": 1748740939, 37 | "narHash": "sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc=", 38 | "owner": "nix-community", 39 | "repo": "nixpkgs.lib", 40 | "rev": "656a64127e9d791a334452c6b6606d17539476e2", 41 | "type": "github" 42 | }, 43 | "original": { 44 | "owner": "nix-community", 45 | "repo": "nixpkgs.lib", 46 | "type": "github" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-parts": "flake-parts", 52 | "nixpkgs": "nixpkgs", 53 | "treefmt-nix": "treefmt-nix" 54 | } 55 | }, 56 | "treefmt-nix": { 57 | "inputs": { 58 | "nixpkgs": [ 59 | "nixpkgs" 60 | ] 61 | }, 62 | "locked": { 63 | "lastModified": 1748243702, 64 | "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", 65 | "owner": "numtide", 66 | "repo": "treefmt-nix", 67 | "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", 68 | "type": "github" 69 | }, 70 | "original": { 71 | "owner": "numtide", 72 | "repo": "treefmt-nix", 73 | "type": "github" 74 | } 75 | } 76 | }, 77 | "root": "root", 78 | "version": 7 79 | } 80 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Development environment for this project"; 3 | 4 | inputs = { 5 | nixpkgs.url = "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz"; 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | 8 | treefmt-nix.url = "github:numtide/treefmt-nix"; 9 | treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | 12 | outputs = 13 | inputs@{ flake-parts, ... }: 14 | flake-parts.lib.mkFlake { inherit inputs; } ( 15 | { ... }: 16 | { 17 | imports = [ ./treefmt.nix ]; 18 | systems = [ 19 | "aarch64-linux" 20 | "x86_64-linux" 21 | "riscv64-linux" 22 | 23 | "x86_64-darwin" 24 | "aarch64-darwin" 25 | ]; 26 | perSystem = 27 | { config, pkgs, ... }: 28 | { 29 | packages.ssh-to-pgp = pkgs.callPackage ./default.nix { }; 30 | packages.default = config.packages.ssh-to-pgp; 31 | 32 | checks.ssh-to-pgp = config.packages.ssh-to-pgp; 33 | checks.lint = pkgs.callPackage ./lint.nix { inherit (config.packages) ssh-to-pgp; }; 34 | }; 35 | } 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Mic92/ssh-to-pgp 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/ProtonMail/go-crypto v1.3.0 9 | golang.org/x/crypto v0.38.0 10 | ) 11 | 12 | require ( 13 | github.com/cloudflare/circl v1.6.0 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= 2 | github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 3 | github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= 4 | github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 5 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 6 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 7 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 8 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 9 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 10 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 11 | -------------------------------------------------------------------------------- /lint.nix: -------------------------------------------------------------------------------- 1 | { ssh-to-pgp, golangci-lint }: 2 | ssh-to-pgp.overrideAttrs (old: { 3 | name = "golangci-lint"; 4 | nativeBuildInputs = old.nativeBuildInputs ++ [ golangci-lint ]; 5 | buildPhase = '' 6 | HOME=$TMPDIR golangci-lint run --timeout 360s 7 | ''; 8 | doCheck = false; 9 | installPhase = '' 10 | touch $out $unittest 11 | ''; 12 | fixupPhase = ":"; 13 | }) 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/ProtonMail/go-crypto/openpgp" 11 | "github.com/ProtonMail/go-crypto/openpgp/armor" 12 | ) 13 | 14 | type options struct { 15 | format, out, in, name, comment, email string 16 | privateKey bool 17 | } 18 | 19 | func parseFlags(args []string) options { 20 | var opts options 21 | f := flag.NewFlagSet(args[0], flag.ExitOnError) 22 | f.BoolVar(&opts.privateKey, "private-key", false, "Export private key instead of public key") 23 | f.StringVar(&opts.format, "format", "armor", "GPG format encoding (binary|armor)") 24 | f.StringVar(&opts.in, "i", "-", "Input path. Reads by default from standard output") 25 | f.StringVar(&opts.out, "o", "-", "Output path. Prints by default to standard output") 26 | f.StringVar(&opts.name, "name", "root", "Name to set for the PGP user id") 27 | f.StringVar(&opts.comment, "comment", "Imported from SSH", "Comment to set for the PGP user id") 28 | f.StringVar(&opts.email, "email", "root@localhost", "Email to set for the PGP user id") 29 | if err := f.Parse(args[1:]); err != nil { 30 | // should never happen since flag.ExitOnError 31 | panic(err) 32 | } 33 | 34 | return opts 35 | } 36 | 37 | func convertKeys(args []string) error { 38 | opts := parseFlags(args) 39 | var err error 40 | var sshKey []byte 41 | if opts.in == "-" { 42 | sshKey, err = io.ReadAll(os.Stdin) 43 | if err != nil { 44 | return fmt.Errorf("error reading stdin: %w", err) 45 | } 46 | } else { 47 | sshKey, err = os.ReadFile(opts.in) 48 | if err != nil { 49 | return fmt.Errorf("error reading %s: %w", opts.in, err) 50 | } 51 | } 52 | 53 | writer := io.WriteCloser(os.Stdout) 54 | if opts.out != "-" { 55 | writer, err = os.Create(opts.out) 56 | if err != nil { 57 | return fmt.Errorf("failed to create %s: %w", opts.out, err) 58 | } 59 | defer func() { 60 | if err = writer.Close(); err != nil { 61 | fmt.Fprintf(os.Stderr, "failed to close writer: %v", err) 62 | } 63 | }() 64 | } 65 | 66 | if opts.format == "armor" { 67 | keyType := openpgp.PublicKeyType 68 | if opts.privateKey { 69 | keyType = openpgp.PrivateKeyType 70 | } 71 | writer, err = armor.Encode(writer, keyType, make(map[string]string)) 72 | if err != nil { 73 | return fmt.Errorf("failed to encode armor writer: %w", err) 74 | } 75 | defer func() { 76 | if err = writer.Close(); err != nil { 77 | fmt.Fprintf(os.Stderr, "failed to close writer: %v", err) 78 | } 79 | }() 80 | } 81 | 82 | gpgKey, err := SSHPrivateKeyToPGP(sshKey, opts.name, opts.comment, opts.email) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if opts.privateKey { 88 | err = gpgKey.SerializePrivate(writer, nil) 89 | } else { 90 | err = gpgKey.Serialize(writer) 91 | } 92 | if err == nil { 93 | fmt.Fprintf(os.Stderr, "%s\n", hex.EncodeToString(gpgKey.PrimaryKey.Fingerprint[:])) 94 | } 95 | return err 96 | } 97 | 98 | func main() { 99 | if err := convertKeys(os.Args); err != nil { 100 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 101 | os.Exit(1) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "path/filepath" 9 | "runtime" 10 | "testing" 11 | ) 12 | 13 | // ok fails the test if an err is not nil. 14 | func ok(tb testing.TB, err error) { 15 | if err != nil { 16 | _, file, line, _ := runtime.Caller(1) 17 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 18 | tb.FailNow() 19 | } 20 | } 21 | 22 | func TempRoot() string { 23 | if runtime.GOOS == "darwin" { 24 | // macOS make its TEMPDIR long enough for unix socket to break 25 | return "/tmp" 26 | } else { 27 | return os.TempDir() 28 | } 29 | } 30 | 31 | func TestCli(t *testing.T) { 32 | assets := os.Getenv("TEST_ASSETS") 33 | if assets == "" { 34 | assets = "test-assets" 35 | } 36 | tempdir, err := os.MkdirTemp(TempRoot(), "testdir") 37 | ok(t, err) 38 | defer func() { 39 | if err = os.RemoveAll(tempdir); err != nil { 40 | fmt.Println("failed to remove tempdir:", err) 41 | } 42 | }() 43 | 44 | gpgHome := path.Join(tempdir, "gpg-home") 45 | gpgEnv := append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gpgHome)) 46 | ok(t, os.Mkdir(gpgHome, os.FileMode(0o700))) 47 | 48 | out := path.Join(tempdir, "out") 49 | privKey := path.Join(assets, "id_rsa") 50 | cmds := [][]string{ 51 | {"ssh-to-pgp", "-i", privKey, "-o", out}, 52 | {"ssh-to-pgp", "-format=binary", "-i", privKey, "-o", out}, 53 | {"ssh-to-pgp", "-private-key", "-i", privKey, "-o", out}, 54 | {"ssh-to-pgp", "-format=binary", "-private-key", "-i", privKey, "-o", out}, 55 | } 56 | for _, cmd := range cmds { 57 | // Make sure we clean the states between each command 58 | exec.Command("rm", "-rf", gpgHome+"/*") 59 | err = convertKeys(cmd) 60 | ok(t, err) 61 | cmd := exec.Command("gpg", "--with-fingerprint", "--show-key", out) 62 | cmd.Stdout = os.Stdout 63 | cmd.Stderr = os.Stderr 64 | cmd.Env = gpgEnv 65 | ok(t, cmd.Run()) 66 | // Try to import the key we've produced 67 | cmdImport := exec.Command("gpg", "--import", out) 68 | cmdImport.Stdout = os.Stdout 69 | cmdImport.Stderr = os.Stderr 70 | cmdImport.Env = gpgEnv 71 | ok(t, cmdImport.Run()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json" 3 | } 4 | -------------------------------------------------------------------------------- /scripts/update-vendor-hash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -i bash -p nix -p coreutils -p gnused -p gawk 3 | 4 | set -exuo pipefail 5 | 6 | failedbuild=$(nix build --impure --expr '(with import {}; pkgs.callPackage ./. { vendorHash = ""; })' 2>&1 || true) 7 | echo "$failedbuild" 8 | checksum=$(echo "$failedbuild" | awk '/got:.*sha256/ { print $2 }') 9 | sed -i -e "s|vendorHash ? \".*\"|vendorHash ? \"$checksum\"|" default.nix 10 | -------------------------------------------------------------------------------- /test-assets/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | 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 38 | -----END OPENSSH PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /treefmt.nix: -------------------------------------------------------------------------------- 1 | { inputs, ... }: 2 | { 3 | imports = [ inputs.treefmt-nix.flakeModule ]; 4 | 5 | perSystem = 6 | { pkgs, ... }: 7 | { 8 | treefmt = { 9 | # Used to find the project root 10 | projectRootFile = "flake.lock"; 11 | flakeCheck = pkgs.hostPlatform.system != "riscv64-linux"; 12 | 13 | programs = { 14 | deno.enable = true; 15 | gofumpt.enable = true; 16 | deadnix.enable = true; 17 | nixfmt-rfc-style.enable = true; 18 | }; 19 | }; 20 | }; 21 | } 22 | --------------------------------------------------------------------------------