Verify an Assertion signature without snapd

Hi,

I would like to verify the signature on a signed serial assertion without snapd being available. I understand it’s based on a pgp signature, so I want to be able to extract that signature and verify is using the public part of the pgp key used to sign it.

From the docs https://core.docs.ubuntu.com/en/reference/assertions/serial , the signature is the part at the end of the assertion document, separated by a new line after the yaml. However, it seems to be encoded somehow, and I’m not sure how to extract it. What format is the encoding, and how can I decode it to extract to to find the signature ?

Cheers,
Just

Well, I decided to take this on as a challenge, and I’m most of the way there.

After digging through the snapd source, I found that encoded signature was a PGP Packet. Not something I’m familiar with, so it took some googling. The real nitty gritty is here of course: https://tools.ietf.org/html/rfc4880#section-4

However, even after finding that nugget, I was unable to extract the data. I’m not very well versed in go, so I wasn’t really able to just use the libraries and functions from snapd. I found a good python library for handling PGP packets, but the data wasn’t loading correctly, failing with this error:

# 7th bit of the first byte must be a 1

Anyway, eventually I noticed this in the decodeV1 function from asserts/crypto.go in snapd code:

rd := bytes.NewReader(buf[1:])

Turns out I just had to discard the first byte ! Not sure why. If anyone knows I’d be interested. Anyhow, I can now extract the signature data from the packet, and write it to a file, and gpg can parse it:

gpg: Signature made Mon 14 Dec 2020 10:06:46 GMT
gpg:                using RSA key 0000000000000000
gpg: Can't check signature: No public key

Yay ! - I don’t have the correct public key locally here atm. But it’s parse-able !

Cheers,
Just

Seems I’m close but no cigar.

The pgp data I extracted is missing some key bits of data [ pun intended, and very consusing ! ]:

Hashed Sub: issuer fingerprint

…and…

Sub: issuer key ID

Investigation continues …

If anyone can help I would still appreciate it :slight_smile:

Cheers,
Just

The first byte is our own versioning in case we switch to something else. The use of gpg is somewhat internal. Anyway we use gpg as a packet format and for the basic algorithms but we don’t use the gpg way to match signature to keys, because we have our own approach to the tree of trust through the account-key assertions. What tools can you use here, python? just shell? gpg?

What you are trying to do is doable but there are moving parts.

@pedronis Thanks for your response :slight_smile:

Regarding the first byte, yes I noticed this earlier, which made me think the first byte was perhaps some kind of custom versioning:

if buf[0] != v1 

I somehow missed that yesterday.

For tooling, I’m currently using python, but am open to anything I guess. I haven’t used golang that much myself.

Can you expand on what you mean by moving parts in this case ?

If you can give me some pointers regarding extracting the signature I’d really appreciate it.

Cheers,
Just

in what form do you have the signing key available? do you have it locally or would need to fetch it from the store? is this just for serial assertions?

It’s a very limited use case, only for serial assertions.

I have the signing key. I would just like the use the public part of that to verify that the signature on the serial assertion is from the private part of the signing key. The serial assertion and public key would be on the same system, but not on a UC host [ or anything running snapd ].

Cheers,
Just

while we do use sometimes gpg (the program) in our signing toolchain, it doesn’t have quite the right affordances to be used to verify the signatures, you need a lower-level library, I haven’t looked/I’m not sure such a library is readily available for python.

it should be possible to do what you want with a small Go program to compile and ship where used. that program could either:

Thanks for pointers. I will try and have a dig through the snapd code.

I do feel like I’m almost there with python though. So I’ll just quickly ask very specifically, do you know why the gpg packet data I was able to parse seems to be missing the key ID and issuer fingerprint ?

When I load a “standard” signature, the data is there. Do I have to do something with the signature data block from the assertion before or after I parse it ?

Cheers,
Just

ok, I have something working. I did have to use go in the end, which made it a bit harder as I have no previous experience, but I got there.

There are python libs for low level pgp packet handling, but they are mostly old and seem unmaintained sadly. Now that I understand the internals a bit better, I could have a go at re-implementing in python, but it wouldn’t be guaranteed outcome and I have something working so I’ll stick with it for now.

I’m using some functionality from the asserts package [ as suggested ] , and some re-implementations of parts of the crypto.go file to make it work. If I new go better, I would probably have done it more elegantly :wink:

Cheers,
Just

If you have issues using parts of the asserts package we expose in snapd, I’d be curious to know what you needed to change to use it for your use cases so we can make the package more user-friendly. PR’s are also always welcome and we’re happy to help writing unit tests for new features, etc. if you’re not quite up to speed on Go development.