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

atproto/crypto: in-repo SDK for atproto cryptography #270

Merged
merged 13 commits into from
Aug 24, 2023

Conversation

bnewbold
Copy link
Collaborator

This borrows some code from https://github.com/whyrusleeping/go-did, but has a refactored API, exposes fewer internals, and has a couple differences:

  • P-256 "low-S" signatures are enforced (when signing and verifying), in addition to just K-256. This fixes an interop test failure vs the typescript repo
  • P-256 binary private key serialization is the normal compact representation (32 bytes), instead of using crypto/x509 with a wrapping format
  • docs and examples

Looking for API-level feedback, and thoughts on the safety/risk of the "low-S" hack code. We could consider asking external folks for API feedback as well, or just iterate on the API later.

If this looks good, I would imagine merging as-is, and then refactoring the code base to use this code instead of go-did as a separate PR. One operational concern i'd want to address is the private key serialization format used in our infra: do we use P-256 private keys, or K-256 private keys, or actually no private keys at all yet?

Can preview godoc HTML output here: https://static.bnewbold.net/tmp/atproto_crypto_godoc.html

cc: @whyrusleeping @ericvolp12

…blicKey

This simplifies the API a bit by making "uncompressed" and "legacy
multibase" special cases.
Copy link
Collaborator

@ericvolp12 ericvolp12 left a comment

Choose a reason for hiding this comment

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

This interface and the GoDoc look pretty good to me.

I'm not a cryptographer but I have strong opinions about never writing a panic in Go code, especially in a library, so I've requested you remove those and make those functions return errors even if it's less ergonomic.

There was a spot where we might wanna bounds check too.

Overall, looks great and I'm happy to have nice ergonomic functions to work with the strange collection of key representations we've gotta deal with.

atproto/crypto/keys.go Outdated Show resolved Hide resolved
atproto/crypto/keys.go Outdated Show resolved Hide resolved
atproto/crypto/keys.go Outdated Show resolved Hide resolved
atproto/crypto/keys.go Outdated Show resolved Hide resolved
@bnewbold
Copy link
Collaborator Author

@ericvolp12 thanks for the review!

An alternative API, which wouldn't be too hard to update to, would be to define an interface and have separate P256 and K256 implementations of that interface. That would remove a bunch of the panic() conditions by pushing things to the type system. Any thoughts on that?

I would probably name them PublicKey, PrivateKey (though SecretKey is also decent, and PrivateKeyExportable. The exportable-or-not being whether the key material is actually in memory, or something like a yubikey. Having overlapping interfaces makes it possible to wrap non-exportable keys in the future and use them with most other APIs/implementations, which seems good.

@bnewbold
Copy link
Collaborator Author

@ericvolp12 I iterated on the API to have it use interfaces instead of a hacked enum struct.

This got rid of most of the panics. There are two remaining panics in the P-256 implementation. These are relatively safe because I call the same code block in all of construction code paths (which return err) to verify that the routine should not actually panic. This is kind of weird, and also resulted in returning err for Public() which wouldn't otherwise be necessary, so i'm willing to iterate again if we want to truely expunge the panics. I don't have particularly strong opinions and would follow your judgement, it just feels weird that serializing a pubkey to bytes, or any of the dependent string encodings than use the bytes, could return err.

A couple methods are probably going to be deprecated. I guess we could could just not include those here so the new interface is clean and stable? But they might also linger longer or be permanent. I feel comfortable yanking them in a couple months as long as they were marked as such from the begining.

@bnewbold
Copy link
Collaborator Author

After sleeping on it, i'll probably ease the stuff for working with "legacy" DID document (weird multibase encoding, "uncompressed" byte formats, suite names) out of any interface definitions. Will keep the uncompressed helpers on the current public key types, but move the rest to the (forthcoming) identity package.

TODO to update this PR reflecting those changes.

@bnewbold
Copy link
Collaborator Author

@ericvolp12 updated and ready for (final?) review

godoc preview: https://static.bnewbold.net/tmp/atproto_crypto_godoc2.html

@ericvolp12
Copy link
Collaborator

ericvolp12 commented Aug 16, 2023

@ericvolp12 updated and ready for (final?) review

godoc preview: https://static.bnewbold.net/tmp/atproto_crypto_godoc2.html

New interface looks sensible to me, great work!

Only question I have is still around the use of hash[:] instead of just hash in a few functions.

Afaict it's a no-op: https://goplay.tools/snippet/Hvt9pv9jAAX

If that's the case, can we pull the [:] out?

Copy link
Collaborator

@ericvolp12 ericvolp12 left a comment

Choose a reason for hiding this comment

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

Just the one note about using hash[:], otherwise LGTM!

@bnewbold
Copy link
Collaborator Author

@ericvolp12 I had a reply to a review comment above about it, the SHA-256 return is typed. If I try removing the [:] from any of those call points, I get a compiler error:

./k256.go:94:38: cannot use hash (variable of type [32]byte) as []byte value in argument to k.privK256.Sign

@bnewbold
Copy link
Collaborator Author

Also just because it felt sneaky, to ensure you noticed, I replaced the "should be impossible" panic calls with fmt.Fatal. I'm not sure if that is actually any better.

@ericvolp12
Copy link
Collaborator

Also just because it felt sneaky, to ensure you noticed, I replaced the "should be impossible" panic calls with fmt.Fatal. I'm not sure if that is actually any better.

I don't think it should be possible for this library to make your program exit

@ericvolp12
Copy link
Collaborator

@ericvolp12 I had a reply to a review comment above about it, the SHA-256 return is typed. If I try removing the [:] from any of those call points, I get a compiler error:

./k256.go:94:38: cannot use hash (variable of type [32]byte) as []byte value in argument to k.privK256.Sign

Ah, okay yeah I missed that, interesting...

@ericvolp12
Copy link
Collaborator

ericvolp12 commented Aug 16, 2023

Also just because it felt sneaky, to ensure you noticed, I replaced the "should be impossible" panic calls with fmt.Fatal. I'm not sure if that is actually any better.

I don't think it should be possible for this library to make your program exit

To elaborate on this, afaik the general convention in Go is that "panics should never leave your code" so if there is a panic somewhere, it should be caught by the library and handled appropriately, not bubbled up to the caller.

I don't see any reason why a cryptography library should make someone's CLI or long-running service suddenly exit without warning. Even in a "this should never happen" case imo, that's what error handling is for.

Calling os.exit either by panic or a .Fatal method should only happen inside the main logic of a program, not in a library.

@bnewbold
Copy link
Collaborator Author

bnewbold commented Aug 16, 2023

@ericvolp12 glad I took another pass, because I found a way to remove any panic / fmt.Fatal and also retain the current simpler API surface (no new error returns) by pre-generating type conversions at key parse time.

This is a small memory overhead cost (tens of bytes per public or private key) for this one specific key type (P-256) to buy the simpler API. If we think we might keep millions of keys in memory I can update the API to return errors in more places instead. Sorry for all the go around on this... maybe i'm tilting at windmills trying to keep the API simple, but, eg, the underlying K-256 implementation has no error returns for any of these routines, and I strongly suspect the K-256 would never possibly actually error on these conditions either.

@ericvolp12
Copy link
Collaborator

ericvolp12 commented Aug 16, 2023

How does the new size of a key compare to the previous size of a key in terms of %? Is it a doubling? I use a LRU cache often to stash keys after doing a DID doc lookup for JWT validation and I'm guessing others are doing something similar so keeping hundreds of thousands and/or millions of keys in memory isn't a wild idea necessarily, but things are gonna get too big anyhow to keep all in memory as user count grows so it might be fine.

I'm fine with either approach you mentioned, the current one or returning more errors in places. Maybe Filo will have an opinion though.

Based on double-checking the crypto/ecdsa stdlib interface, Public()
often returns a crypto.Public, and PublicKey() is used to return
specifically the package-local PublicKey type.
@bnewbold
Copy link
Collaborator Author

Didn't actually need the duplicate key representations for PublicKey, so got rid of that duplication and overhead.

There is still the overhead (2x, I assume) for private keys. It seems less likely that folks would have millions of private keys hanging around in memory (compared to public keys).

I am not strongly against just adding error to basically all the return types here to avoid any future issues and leave wiggle room, even though it doesn't seem necessary for now.

@ericvolp12
Copy link
Collaborator

I think having 2x overhead for private keys is fine. Caching a ton of private keys is a lot less common of a scenario than caching a ton of public keys imo.

Copy link
Collaborator

@ericvolp12 ericvolp12 left a comment

Choose a reason for hiding this comment

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

LGTM

@bnewbold bnewbold merged commit 307070e into main Aug 24, 2023
@bnewbold bnewbold deleted the bnewbold/sdk-crypto branch August 24, 2023 22:20
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.

2 participants