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
GPG2 signer #199
Conversation
Pull Request Test Coverage Report for Build 1322
💛 - Coveralls |
@g-k any reason not to go to ubuntu bionic? That has gpg 2.2 |
@hwine per https://travis-ci.community/t/ubuntu-18-04-1-lts-bionic-beaver/1270 Travis CI doesn't support it yet. |
5985202
to
c0b0e1f
Compare
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 |
There was a problem hiding this 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 \ | ||
&& \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bold move
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- move interrupt handling to main
- add a cleanup/teardown interface for signers
- have the main interrupt handler call each that fn for each signer
There was a problem hiding this comment.
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.
Workaround gpg 2.1 on CCI image
// 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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. | ||
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
signer/gpg2/README.rst
Outdated
-----END PGP PRIVATE KEY BLOCK----- | ||
publickey: | | ||
-----BEGIN PGP PUBLIC KEY BLOCK----- | ||
... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing leading spaces
signer/gpg2/gpg2.go
Outdated
|
||
// Passphrase is the optional passphrase to use decrypt the | ||
// gpg secret key | ||
Passphrase string |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
signer/gpg2/gpg2.go
Outdated
} | ||
|
||
func (s *GPG2Signer) AtExit() error { | ||
os.RemoveAll(s.tmpDir) |
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: gofmt
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh that's bizarre I'm seeing : https://screenshots.firefox.com/aPYW8lY8SKavqYSE/github.com
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
r+ w/ nits
huh we aren't getting coverage data https://coveralls.io/jobs/43793714 |
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:
/tmp/autograph_gpg2_<signer_id>
gpg2_publickey
andgpg2_privatekey
in that dir{autograph_gpg2_keyring.gpg,autograph_gpg2_secring.gpg}
in that dirand 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)/tmp/autograph_gpg2_<signer_id>/
and removed at SignData fn exitFunctional Test:
r? @jvehent