-
Notifications
You must be signed in to change notification settings - Fork 44
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
Verify release signatures #102
Conversation
This commit extends the chisel release with keyring definitions. Keyrings are defined in ASCII armored format in the top-level public-keys property by name. Keyrings are referenced by name in the public-keys list property in archive definitions. An example of the extended chisel release file is at the bottom. This commit uses the newly added github.com/ProtonMail/go-crypto/openpgp package dependency[1]. This package is a maintained fork of the deprecated golang.org/x/crypto/openpgp package[2][3]. [1] https://github.com/ProtonMail/go-crypto [2] https://pkg.go.dev/golang.org/x/crypto/openpgp [3] https://golang.org/issue/44226 Example chisel.yaml: format: chisel-v1 archives: ubuntu: version: 22.04 components: [main, universe] suites: [jammy, jammy-updates, jammy-security] public-keys: [ubuntu] ubuntu-fips: version: 22.04 pro: fips components: [main] suites: [jammy] public-keys: [ubuntu-fips] public-keys: ubuntu: | -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFzZxGABEADSWmX0+K//0cosKPyr5m1ewmwWKjRo/KBPTyR8icHhbBWfFd8T DtYggvQHPU0YnKRcWits0et8JqSgZttNa28s7SaSUTBzfgzFJZgULAi/4i8u8TUj +KH2zSoUX55NKC9aozba1cR66jM6O/BHXK5YoZzTpmiY1AHlIWAJ9s6cCClhnYMR ... E+SWDGxtgwixyPziL56UavL/eeYJWeS/WqvGzZzsAtgSujFVLKWyUaRi0NvYW3h/ I50Tzj0Pkm8GtgvP2UqAWvy+iRpeUQ2ji0Nc =j6+P -----END PGP PUBLIC KEY BLOCK----- ubuntu-fips: | -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBE+tgXgBEADfiL1KNFHT4H4Dw0OR9LemR8ebsFl+b9E44IpGhgWYDufj0gaM /UJ1Ti3bHfRT39VVZ6cv1P4mQy0bnAKFbYz/wo+GhzjBWtn6dThYv7n+KL8bptSC Xgg1a6en8dCCIA/pwtS2Ut/g4Eu6Z467dvYNlMgCqvg+prKIrXf5ibio48j3AFvd ... mguPI1KLfnVnXnsT5JYMbG2DCLHI/OIvnpRq8v955glZ5L9aq8bNnOwC2BK6MVUs pbJRpGLQ29hbeH8jnRPOPQ+Sbwa2C8/ZSoBa/L6JGl5RDaOLQ1w= =6Bkw -----END PGP PUBLIC KEY BLOCK-----
This commit builds on the previous commit that introduces support for OpenPGP keyrings to verify signatures of archive release files. Previously, we used the Release file of each configured suite. With this commit, we fetch the InRelease file instead. This file is in clearsign format[1] and contains the Release file and its signature[2]. [1] https://www.gnupg.org/gph/en/manual/x135.html [2] https://www.chiark.greenend.org.uk/~cjwatson/blog/no-more-hash-sum-mismatch-errors.html
This PR depends on #100 The CI will pass once chisel-releases contains valid Ubuntu keyrings. The PRs in chisel-releases are: https://github.com/canonical/chisel-releases/pulls?q=is%3Apr+is%3Aopen+%22Add+Ubuntu+archive+signing+keys%22+in%3Atitle |
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.
Looks quite good, thanks. Only superficial comments.
internal/archive/archive.go
Outdated
for _, keyring := range index.archive.options.Keyrings { | ||
_, err = block.VerifySignature(keyring, nil) | ||
if err == nil || !errors.Is(err, pgperrors.ErrUnknownIssuer) { | ||
break |
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 imagined that our intent here would be that, when we have a list of keys, any one of them being a valid verifier would suffice, but the above seems to do otherwise. Not only that, but it seems to change behavior depending on the order of the public keys as well. What's the idea there?
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 think it works as you think it should no matter how are the keys ordered.
Pasting the whole block in question for full context:
err := pgperrors.ErrUnknownIssuer
for _, keyring := range index.archive.options.Keyrings {
_, err = block.VerifySignature(keyring, nil)
if err == nil || !errors.Is(err, pgperrors.ErrUnknownIssuer) {
break
}
}
if err != nil {
return nil, fmt.Errorf("signature verification failed: %w", err)
}
If the archive defines no keys, we fail to verify. (err
is initially set to pgperrors.ErrUnknownIssuer
.)
If a key in a keyring successfully verifies the signature, then err
is nil, and we break and succeed.
If no key in a keyring verifies the signature, IOW if err
is pgperrors.ErrUnknownIssuer
, we continue with the next keyring.
If any other error occurs during the signature verification, we fail immediately. Here's the list of possible errors: https://github.com/ProtonMail/go-crypto/blob/main/openpgp/errors/errors.go
Am I missing something obvious?
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.
Just realized, that order can indeed matter for fatal errors, i.e. if one keyring has revoked keys, we can get ErrKeyRevoked
and fail immediately, even though next keyring would verify it just fine. In that case, should we continue?
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'd propose that it succeeds if there's at least one valid key, issuing warnings for those that don't work.
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.
What if one of the keyrings contain key revocations? We'd have to go through them all. But then, what if a release is signed by key A
, keyring k1
contains public key A
and keyring k2
contains public key B
and revocation for key A
? Which of these keyrings do we use?
internal/archive/archive.go
Outdated
func (index *ubuntuIndex) fetchRelease() error { | ||
logf("Fetching %s %s %s suite details...", index.label, index.version, index.suite) | ||
reader, err := index.fetch("Release", "", fetchDefault) | ||
reader, err := index.fetchVerifyReleaseFile() |
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.
How about organizing this as:
fetchRelease
=> Just fetches the InRelease file, returnsreader, err
verifyRelease
=> Takesreader
fromfetchRelease
, returnsreader, err
parseRelease
=> CallsfetchRelease
,verifyRelease
, setsindex.release
as appropriate.
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.
Applied with one small variation that instead of passing around a reader, a byte array is passed, because clearsign.Decode()
expects []byte
. If I passed around reader, I'd have to read it fully in verifyRelease
and then create byte reader for parseRelease
. This way, network IO is isolated in fetchRelease
. What do you think? If you like your suggestion better, I'm going to apply it verbatim.
internal/setup/setup.go
Outdated
@@ -30,6 +31,7 @@ type Archive struct { | |||
Version string | |||
Suites []string | |||
Components []string | |||
Keyrings []openpgp.KeyRing |
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.
PublicKeys
"keyring" is the container, which holds public keys, private keys, or whatever else the format supports. For us, these keyrings hold the public side of the keys that signed the archive.
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.
Updated, but I don't think this is proper naming.
IMHO the property in YAML is misnamed because keyring is what it actually holds. It's list of packets that is turned into openpgp.KeyRing
instance which we use for signature verification and it can contain public keys but also revoked keys.
FWIW it can also contain just a private key, which I use for testing here https://github.com/canonical/chisel/pull/102/files/0dd312696da0c165a5d9c42a2312174e48f663e8#diff-dffb7bab06af83e76768dcafc44e1f9b291acf0b33fd280dc65b970a0bba6ed2R119. I use this keyring to create signatures in testarchive
and I also pass it to what is now called PublicKeys
.
Example:
$ gpg --no-default-keyring --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg -k
/usr/share/keyrings/ubuntu-archive-keyring.gpg
----------------------------------------------
pub rsa4096 2012-05-11 [SC]
790BC7277767219C42C86F933B4FE6ACC0B21F32
uid [ unknown] Ubuntu Archive Automatic Signing Key (2012) <[email protected]>
pub rsa4096 2012-05-11 [SC]
843938DF228D22F7B3742BC0D94AA3F0EFE21092
uid [ unknown] Ubuntu CD Image Automatic Signing Key (2012) <[email protected]>
pub rsa4096 2018-09-17 [SC]
F6ECB3762474EDA9D21B7022871920D1991BC93C
uid [ unknown] Ubuntu Archive Automatic Signing Key (2018) <[email protected]>
$ gpg --no-default-keyring --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg --export --armor
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBE+tgXgBEADfiL1KNFHT4H4Dw0OR9LemR8ebsFl+b9E44IpGhgWYDufj0gaM
/UJ1Ti3bHfRT39VVZ6cv1P4mQy0bnAKFbYz/wo+GhzjBWtn6dThYv7n+KL8bptSC
...
E+SWDGxtgwixyPziL56UavL/eeYJWeS/WqvGzZzsAtgSujFVLKWyUaRi0NvYW3h/
I50Tzj0Pkm8GtgvP2UqAWvy+iRpeUQ2ji0Nc
=j6+P
-----END PGP PUBLIC KEY BLOCK-----
$ gpg --no-default-keyring --keyring /usr/share/keyrings/ubuntu-archive-removed-keys.gpg -k
/usr/share/keyrings/ubuntu-archive-removed-keys.gpg
---------------------------------------------------
pub dsa1024 2004-09-12 [SC]
630239CC130E1A7FD81A27B140976EAF437D05B5
uid [ unknown] Ubuntu Archive Automatic Signing Key <[email protected]>
sub elg2048 2004-09-12 [E]
pub dsa1024 2004-12-30 [SC]
C5986B4F1257FFA86632CBA746181433FBB75451
uid [ unknown] Ubuntu CD Image Automatic Signing Key <[email protected]>
$ gpg --no-default-keyring --keyring /usr/share/keyrings/ubuntu-archive-removed-keys.gpg --export --armor
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGiBEFEnz8RBAC7LstGsKD7McXZgd58oN68KquARLBl6rjA2vdhwl77KkPPOr3O
YeSBH/voUsqausJfDNuTNivOfwceDe50lbhq52ODj4Mx9Jg+4aHn9fmRkIk41i2J
...
HcRaDJqO6YL9tsvinm+qciUo3vQVQzF0pptOx49Z0gAHYNU7PULHpvA=
=f4jO
-----END 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.
Thanks for the change @woky. In this case, despite both being accurate, I do agree that PublicKeys
is a better name as it is more explicit about what that variable expects. As @niemeyer said, a Keyring is a container of keys, but in our case, we are restricting that container to only have public keys, and thus it is something much narrower than a keyring.
Being verbose about it will help with readability.
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 are restricting that container to only have public keys
We are not though. See my comments about testing.
EDIT: FYI, the openpgp
package doesn't care whether the armored block type is PGP PUBLIC KEY BLOCK
or PGP PRIVATE KEY BLOCK
(it only checks whether it's either of those two). It just takes the list of things inside and adds them to a keyring.
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.
Sure. Our point is that the chisel release configuration (yaml) expects a list of public keys, and while a collection of public keys can be a keyring, a keyring might not be solely composed of public keys
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.
Here is some trivial code to process an armored public key:
https://gist.github.com/niemeyer/a4c221dff9fd166500db80d4ce9f817a
I'm closing this PR for now as the conversation is not being productive here.
@cjdcordeiro Let's please coordinate on how to continue this work.
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.
Getting a public key from keyring is easy. Using it to verify clearsigned message requires bits from private openpgp code base. And as I said above, trying every key public key against a signature is not the way signatures are supposed to be verified. The keyring abstractions solves this fact. If we used use raw public keys, we'd also need to copy the part that filters those that have given issuer ID matching that of the signature. That's another part of openpgp code we'd need to copy.
So I have conflicting guidance
- Use
openpgp/packet.PublicKey
- We're not going to fork openpgp
We can't do both at the same time.
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.
Getting a public key from keyring is easy.
Everything is easy, code wise. I have code at hand to verify the signature which is just as simple. But this is not the problem anymore. We need someone that is willing to understanding the underlying rationale, instead of me coding the feature with small incomplete snippets while trying to covey an idea here.
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 you mind sharing that code? AFAU, it requires us to copy these blocks of code from the openpgp library:
- https://github.com/ProtonMail/go-crypto/blob/afb1ddc0824c/openpgp/clearsign/clearsign.go#L408-L421
- https://github.com/ProtonMail/go-crypto/blob/afb1ddc0824c/openpgp/clearsign/clearsign.go#L446-L464
- https://github.com/ProtonMail/go-crypto/blob/afb1ddc0824c/openpgp/read.go#L480-L499
IOW, there's no built-in support to verify clearsigned message against a raw public key, we need to copy bits of the library implementation.
I would love to be proven wrong so we could move forward.
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've tried to give example implementation using only packet.PublicKey
another go and it turns out, we'd have to copy more code from the library to be correct. The keys have flags and we also need to filter keys that can sign (verify) messages: https://github.com/ProtonMail/go-crypto/blob/afb1ddc0824ce0052d72ac0d6917f144a1207424/openpgp/keys.go#L338-L366 This information is stored in openpgp.Key
instance in SelfSignature
field. So this keyring machinery would need to be copied to setup.go
.
internal/setup/setup.go
Outdated
@@ -414,6 +418,16 @@ func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { | |||
return nil, fmt.Errorf("%s: no archives defined", fileName) | |||
} | |||
|
|||
keyringsByName := make(map[string]openpgp.KeyRing, 0) |
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 won't repeat this again, but let's please review the PR and update the terminology with some nice var naming (short but neat) so when we're speaking of the content of those fields we refer to them as the archive public keys. If this sounds strange, imagine an age
variable of type int
. We'd refer to it as the "age" rather than the "int".
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've updated the naming where it makes sense. For instance, in archive_test.go
, where I use the same keyring for creating and verifying signatures, I think it doesn't make sense to call it PublicKeys
. That's just a suggestion though, I don't mind changing it everywhere.
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 think that makes sense @woky ...if your test key is more than a public key, then it shouldn't be called PublicKey
Unfortunately, it appears that |
I've fixed the issue. Variables/functions/comments/messages are updated to refer to "public keys" instead of keyrings. I retained the |
This commit builds on the previous commit that introduces support for
OpenPGP keyrings to verify signatures of archive release files. Previously,
we used the Release file of each configured suite. With this commit, we
fetch the InRelease file instead. This file is in clearsign format[1] and
contains the Release file and its signature[2].
[1] https://www.gnupg.org/gph/en/manual/x135.html
[2] https://www.chiark.greenend.org.uk/~cjwatson/blog/no-more-hash-sum-mismatch-errors.html