Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPG2 signer #199

Merged
merged 31 commits into from Jan 4, 2019
Merged

GPG2 signer #199

merged 31 commits into from Jan 4, 2019

Conversation

g-k
Copy link
Contributor

@g-k g-k commented Dec 19, 2018

Changes:

  • Adds a new signer with type gpg2 that shells out to a gpg2 binary to handle the RelEng use case and as a reference for a pure golang implementation if we decide to do that in the future. Per the signer readme it expects a passphrase, key ID, and public key in addition to the armored private key.

  • Changes Travis CI to build a container and run tests inside it (we need gpg > 2.1 and Travis CI doesn't support the Ubuntu Bionic base image where it's the default tracked in https://travis-ci.community/t/ubuntu-18-04-1-lts-bionic-beaver/1270 I didn't see a 2.2 pkg in backports and continuing to mess around with aliasing or building from source seemed worse)

  • Adds an autograph config with just the APK signers for the CircleCI testsignapk job to workaround CircleCI only having gpg 2.1 in (which resulted in a permissions error creating the subkey at test gpg2 signer init)

NB: this will require a monitor deploy since it adds a new signer type

Implementation-wise:

  • at signer init / app start we
    • create a temp dir /tmp/autograph_gpg2_<signer_id>
    • write public and private keys to gpg2_publickey and gpg2_privatekey in that dir
    • import the keys into gpg2 key and secrings
    • this creates {autograph_gpg2_keyring.gpg,autograph_gpg2_secring.gpg} in that dir and the keys are not deleted at app exit (since we're assuming we're running in a container, but someone running locally with sensitive keys might want to clean those up)
  • For each request input data is written to a tempfile in /tmp/autograph_gpg2_<signer_id>/ and removed at SignData fn exit
  • when that app is terminated with SIGTERM, or the platform-generalized os.Interrupt, os.Kill signals (which should translate to SIGINT and SIGKILL on unix) the signer's temp dir is cleaned up (edit: SIGKILL isn't caught)

Functional Test:

  • go through the steps in the readme to sign and verify a detached signature using the subkey

r? @jvehent

@coveralls
Copy link

coveralls commented Dec 19, 2018

Pull Request Test Coverage Report for Build 1322

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at ?%

Totals Coverage Status
Change from base Build 1312: 0.0%
Covered Lines:
Relevant Lines: 0

💛 - Coveralls

@hwine
Copy link
Contributor

hwine commented Dec 27, 2018

@g-k any reason not to go to ubuntu bionic? That has gpg 2.2

@g-k
Copy link
Contributor Author

g-k commented Dec 27, 2018

@hwine per https://travis-ci.community/t/ubuntu-18-04-1-lts-bionic-beaver/1270 Travis CI doesn't support it yet.

autograph.yaml Show resolved Hide resolved
@g-k g-k force-pushed the gpg-cli branch 3 times, most recently from 5985202 to c0b0e1f Compare December 27, 2018 23:03
@g-k g-k requested a review from jvehent December 27, 2018 23:14
@g-k g-k changed the title PGP CLI signer GPG2 signer Dec 27, 2018
@g-k
Copy link
Contributor Author

g-k commented Dec 27, 2018

NB: The Travis CI jobs have a failing test, but the overall CI job is passing (maybe due to moving the test inside a container)

--- FAIL: TestStartMain (0.36s)
    main_test.go:296: Get http://localhost:8000/__heartbeat__: dial tcp 127.0.0.1:8000: connect: connection refused

jvehent
jvehent previously approved these changes Dec 28, 2018
Copy link
Contributor

@jvehent jvehent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, modulo the concern on having the secring on disk.

RUN addgroup --gid 10001 app && \

RUN addgroup --gid 10001 app \
&& \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bold move

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixes a docker warning

@@ -0,0 +1,219 @@
server:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this new config file needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because creating the keyring on app start depends on gpg2 > 2.1 and this was a workaround for trying to get that installed on the test image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove it when either the testapksigner image circle uses a gpg2 > 2.1 or we switch to running the tests against containerized autograph

// call gpg to load the private key in it
gpgLoadPrivateKey := exec.Command("gpg", "--no-default-keyring",
"--keyring", keyRingPath,
"--secret-keyring", secRingPath,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concern that we're putting the secring on disk with no way to clean it up should autograph crash and the instance get recycled. Other signers keep their configurations in memory so we have no artifact left on disk that could eventually leak secrets. This is harder to do here, because I don't think Go can register a function to run on shutdown/crash. Maybe the passphrase protects the key sufficiently that we don't have to worry about it? Or maybe we can put the secring on a tmpfs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in the tmp dir for the signer.

With Python I'd register an atexit handler. With golang we can handle interrupts and clean up on sigterm, sigint, and sigkill though there might be ways to have it not run and I'd expect docker to duplicate the clean up when it's run in container.

Alternatively, the initial implementation 5a4c7ac#diff-5c7805a5493d8e612e5a2c5e5974d363L70 creates and cleans up the key and sec rings for each run, so we could revert to that with the caveats that 1) we'd need to namespace and manage rings per request (e.g. by include the request ID in the files) and 2) it would increase per request latency since we're doing more IO for each request. That might be cleanest though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't catch sigkill, so that leaves a uncovered corner case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK added a cleanup handler and checked that the temp dir is cleaned up.

For multiple gpg2 signers or if other signers add a cleanup handler that calls os.Exit we create a race condition. Assuming cleanup is the way we want to go we'll want to:

  1. move interrupt handling to main
  2. add a cleanup/teardown interface for signers
  3. have the main interrupt handler call each that fn for each signer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK went ahead and made the above change so we can support multiple gpg2 signers and confirmed that kill -SIGKILL did leave the rings around.

I think our advice should be to run autograph in the container in general and especially if you're testing dep or prod gpg keys, since we've expanded the app boundary to include the filesystem and gpg.

// signals and run signer AtExit functions
func (a *autographer) startCleanupHandler() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGTERM)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this catch autograph itself panicking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should just catch the int, kill, and term signals (though Hal pointed out that kill isn't catchable).

If there's a way to panic and trigger those signals I assume they'd be caught, but other signals
like SIGBUS, SIGFPE, and SIGSEGV get converted into runtime panics as usual.

This does mean we'd leave the sec and key rings on disk, but I think they'd be helpful for debugging and don't want to mask actual panics.

https://golang.org/pkg/os/signal/#hdr-Types_of_signals

passphrases exported with the `unsupported and non-standard gnu-dummy
S2K algorithm`_ and signing with subkeys. However, it does require
that a gpg2 version > 2.1 be installed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add: gpg2 supports hesoteric key formats that the pgp signer doesn't understand, but it comes at the cost of writing private keys to disk in order to shell out to the gnupg binary. The pgp signer will keep keys in memory at all times, which is more secure. Try using it first and only switch to gpg2 if needed.

- id: some-pgp-key
type: gpg2
keyid: 0xE09F6B4F9E6FDCCB
passphrase: abcdef123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the passphrase be omitted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be the empty string, but we're assuming there's a passphrase and keyid for this signer and people will use the golang one otherwise.

-----END PGP PRIVATE KEY BLOCK-----
publickey: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing leading spaces


// Passphrase is the optional passphrase to use decrypt the
// gpg secret key
Passphrase string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do those need to be exported?

gpgLoadPublicKey := exec.Command("gpg",
"--no-default-keyring",
"--keyring", keyRingPath,
"--secret-keyring", secRingPath,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we care about command injection via malicious config files? I'm thinking not, but the paranoid in me still thinks enforcing alphanumerical here would be good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't hurt. The paths are built from the signer Type (hardcoded) and KeyID (added alphanumeric validation in New), golang TempDir (can trust), and hardcoded file names. I suppose we could additionally validate them at each call site in case how they're constructed changes.

}

func (s *GPG2Signer) AtExit() error {
os.RemoveAll(s.tmpDir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeall can return an error, so maybe just do return os.RemoveAll(s.tmpDir) ?

Mode string `json:"mode"`
PrivateKey string `json:"privatekey,omitempty"`
PublicKey string `json:"publickey,omitempty"`
Certificate string `json:"certificate,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: gofmt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gofmt -d signer/signer.go isn't giving me anything

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webui quirk? the string of Certificate is misaligned: https://screenshots.firefox.com/lz6CVo8LSu2Rb7GH/github.com

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvehent
jvehent previously approved these changes Jan 3, 2019
Copy link
Contributor

@jvehent jvehent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r+ w/ nits

@g-k
Copy link
Contributor Author

g-k commented Jan 3, 2019

huh we aren't getting coverage data https://coveralls.io/jobs/43793714

@g-k g-k merged commit 2307a78 into master Jan 4, 2019
@g-k g-k mentioned this pull request Jan 14, 2019
@g-k g-k deleted the gpg-cli branch September 24, 2019 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants