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

ADR 028: Public Key Addresses #7086

Merged
merged 18 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ Please add a entry below in your Pull Request for an ADR.
- [ADR 023: Protocol Buffer Naming and Versioning](./adr-023-protobuf-naming.md)
- [ADR 024: Coin Metadata](./adr-024-coin-metadata.md)
- [ADR 025: IBC Passive Channels](./adr-025-ibc-passive-channels.md)
- [ADR 026: IBC Client Recovery Mechanisms](./adr-026-ibc-client-recovery-mechanisms.md)
- [ADR 026: IBC Client Recovery Mechanisms](./adr-026-ibc-client-recovery-mechanisms.md)
- [ADR 028: Public Key Addresses](./adr-028-public-key-addresses.md)
110 changes: 110 additions & 0 deletions docs/architecture/adr-028-public-key-addresses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# ADR 028: Public Key Addresses

## Changelog

- 2020/08/18: Initial version

## Status

Proposed

aaronc marked this conversation as resolved.
Show resolved Hide resolved
## Context

Issue [\#3685](https://github.com/cosmos/cosmos-sdk/issues/3685) identified that public key
addresses are currently overlapping. One initial proposal was extending the address length and
adding prefixes for different types of addresses.

@ethanfrey explained an alternate approach originally used in https://github.com/iov-one/weave:

> I spent quite a bit of time thinking about this issue while building weave... The other cosmos Sdk.

> Basically I define a condition to be a type and format as human readable string with some binary data appended. This condition is hashed into an Address (again at 20 bytes). The use of this prefix makes it impossible to find a preimage for a given address with a different condition (eg ed25519 vs secp256k1).

> This is explained in depth here https://weave.readthedocs.io/en/latest/design/permissions.html

> And the code is here, look mainly at the top where we process conditions. https://github.com/iov-one/weave/blob/master/conditions.go

And explained how this approach should be sufficiently collision resistant:
> Yeah, AFAIK, 20 bytes should be collision resistance when the preimages are unique and not malleable. A space of 2^160 would expect some collision to be likely around 2^80 elements (birthday paradox). And if you want to find a collision for some existing element in the database, it is still 2^160. 2^80 only is if all these elements are written to state.

> The good example you brought up was eg. a public key bytes being a valid public key on two algorithms supported by the codec. Meaning if either was broken, you would break accounts even if they were secured with the safer variant. This is only as the issue when no differentiating type info is present in the preimage (before hashing into an address).

> I would like to hear an argument if the 20 bytes space is an actual issue for security, as I would be happy to increase my address sizes in weave. I just figured cosmos and ethereum and bitcoin all use 20 bytes, it should be good enough. And the arguments above which made me feel it was secure. But I have not done a deeper analysis.
Copy link
Collaborator

@robert-zaremba robert-zaremba Oct 2, 2020

Choose a reason for hiding this comment

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

I don't think the prefix will solve the security problem here. I was reading #3685 and I'm not sure if the solution solves the security problem. Here is my reasoning (maybe it's wrong):

Let's say we have to PK algorithms: A and B. For a user with (pk1, sk1) key pair, we have two possible attacks:

  1. A became vulnerable. Attacker is able to create a valid signature without knowing sk1. In this case we don't solve anything with this proposal.
  2. Attacker is able to find a different key pair (pk2, sk2), possible belonging to a different PK scheme, such that address(typ(pk2), pk2) == address(typ(pk1), pk1). Then he basically broke the cryptographic hash function. The key type (and the prefix) is not important here, because the attacker has an algorithm how to find an pre-image.

Copy link
Collaborator

Choose a reason for hiding this comment

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

To be clear - I'm not saying that adding prefix is bad. I'm not sure it solves anything. @ethanfrey ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

There is one more attack, which in fact is important here, and this update address it:

  1. Similar to (1.): A became vulnerable. Attacker is able to forge a signature for pk \in A. In all places where we don't store any relationship between addresses and (PK, PK scheme) pair, the attacker will be able to spend address assets.

This proposal (including scheme url / name in the address algorithm) protects against the attack described #3685 (isolating address spaces to protect against attacks when one of the PK scheme is broken).


In discussions in [\#5694](https://github.com/cosmos/cosmos-sdk/issues/5694), we agreed to go with an
approach similar to this where essentially we take the first 20 bytes of the `sha256` hash of
the key type concatenated with the key bytes, summarized as `Sha256(KeyTypePrefix || Keybytes)[:20]`.

## Decision

### Legacy Public Key Addresses Don't Change
aaronc marked this conversation as resolved.
Show resolved Hide resolved

`secp256k1` and multisig public keys are currently in use in existing Cosmos SDK zones. We
don't want to change existing addresses. So the addresses for these two key types will remain the same.
aaronc marked this conversation as resolved.
Show resolved Hide resolved

The current multisig public keys use amino serialization to generate the address. We will retain
aaronc marked this conversation as resolved.
Show resolved Hide resolved
those public keys and their address formatting, and call them "legacy amino" multisig public keys
in protobuf. We will also create multisig public keys without amino addresses to be described below.

### Canonical Public Key Addresses

Following on the discussion in [\#5694](https://github.com/cosmos/cosmos-sdk/issues/5694), we propose the
following approach.

All public key types will have a unique protobuf message type such as:
aaronc marked this conversation as resolved.
Show resolved Hide resolved

```proto
package cosmos.crypto.sr25519;

message PubKey {
bytes key = 1;
}
```

All protobuf messages have unique fully qualified names, in this example `cosmos.crypto.sr25519.PubKey`.
aaronc marked this conversation as resolved.
Show resolved Hide resolved
These names are derived directly from .proto files in a standardized way and used
in other places such as the type URL in `Any`s. Since there is an easy and obvious
way to get this name for every protobuf type, we can use this message name as the
key type prefix when creating addresses.

We define the canonical address format for new (non-legacy) public keys as
`Sha256(fmt.Sprintf("%s/%x, proto.MessageName(key), key.Bytes())[:20]`. This takes
Copy link
Contributor

Choose a reason for hiding this comment

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

Current secp256k1 uses ripemd160 instead of [:20]. The latter is faster to compute, but are there other trade-offs (in particular, security-wise)?

Copy link
Contributor

Choose a reason for hiding this comment

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

It is needed for bitcoin compatibility. There were other designs, but the 2017 fundraiser addresses were generated with a bitcoin secp256k1 address derivation and the go code had to be compatible.

But I cannot tell you if 20 bytes prefix is just as safe

Copy link
Member

Choose a reason for hiding this comment

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

ripemd160 is an odd and basically pointless cryptographic choice and there is no reason to retain it.

It's a slow hash function and doesn't improve security in a meaningful way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we are changing this, how about using blake2b? Which is faster, and many believe that it's more secure.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why not blake3 @robert-zaremba ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

So hypothetically it would be easier to find a pre-image for a SHA256 address than a Blake2 address?

Not worse, and it's more random.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Blake3 reduced the number of rounds from Blake2 to get a speed up

It uses a different mechanism, so I can't say if the reduction of number of rounds impacts the security vs Blake2 or not. Probably asking one of the authors (Zooko?) would be the best approach.

Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe we err on the side of caution and go with Blake2

This seams to be the balanced solution. Although it would be still good to check out with zcash about their view. I will try to reach out to some of them.

Copy link
Member Author

Choose a reason for hiding this comment

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

The new draft uses blake2b. Both because it has better library support and has been around longer and vetted more thoroughly (as I understand it).

I'm assuming blake2b is preferable to blake2s for our use case?

Choose a reason for hiding this comment

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

BLAKE2b is designed and optimized for 64-bit CPUs. BLAKE2s is designed and optimized for 8, 16, 32-bit CPUs.
BLAKE2 has indeed excellent Golang and Rust libraries support. Today there are very few BLAKE3 supported implementations. The speed boost observed with BLAKE3 is not really a selling point regarding ADR 028.

aaronc marked this conversation as resolved.
Show resolved Hide resolved
the first 20 bytes of an SHA-256 hash of a string with the proto message name for the key
type joined by an `/` with the hex encoding of the key bytes.
Copy link
Member

@ebuchman ebuchman Sep 20, 2020

Choose a reason for hiding this comment

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

Hex encoding the pubkey bytes in the preimage seems a bit strange actually. Usually we're hashing raw pubkey bytes, so hex encoding will double the size of the pubkey being hashed. Not that I'd really be worried about the performance, more so the weirdness of hex-encoding bytes before they're hashed. Even if we're prefixing with a string, I would think Sha256(fmt.Sprintf("%s/%s, proto.MessageName(key), key.Bytes())[:20] is fine?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, why bother with the / either? In #5694 we talked about Sha256(KeyTypePrefix || Keybytes)[:20] which is just pure concatenation. I guess because the prefix is a string not just a byte, so we need a domain separator? It seems like maybe we're optimizing for human readability of the pre-image; is that important for something?

Copy link
Collaborator

Choose a reason for hiding this comment

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

PubKeys are not meant to be human readable. They are long. That's why we are creating addresses.

So we don't even need to convert to string. We can concatenate bytes, without conversion to string.

Copy link
Member Author

@aaronc aaronc Oct 8, 2020

Choose a reason for hiding this comment

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

I've updated this ADR to define an AddressHash function which takes a string prefix and binary contents and separates them with a null-terminator 0 byte without hex-encoding the binary. Can you take a look again @ebuchman ?


For all basic public keys, key bytes should just be the raw unencoded public key bytes.

### Multisig Addresses
aaronc marked this conversation as resolved.
Show resolved Hide resolved

For new multisig public keys, we define a custom address format not based on any encoding scheme
(amino or protobuf).

First we define a proto message for multisig public keys:
```proto
package cosmos.crypto.multisig;

message PubKey {
uint32 threshold = 1;
repeated google.protobuf.Any public_keys = 2;
}
```

Each nested public key has its own address, so we can use that address as a starting
point for forming the multisig address. Let's create an array of strings, `hexAddresses []string`,
which is the hex-encoded address of each nested pubkey. We join these hex encoded addresses
with a `/`, i.e. `joinedHexAddresses := strings.Join(hexAddresses, "/")`. We then form the address of the multisig pubkey,
using `Sha256(fmt.Sprintf("cosmos.crypto.multisig.PubKey/%d/%s", pk.Threshold, joinedHexAddresses)[:20]`.
Copy link
Member

Choose a reason for hiding this comment

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

Same thoughts as above re hex (and maybe the "/"s).

Why not proto-encode the above PubKey structure here and otherwise use the same address format as the non-multisig, where the pubkey bytes are just the proto encoding of the Proto multisig PubKey structure?

Each nested PubKey would have to be typed too, but that seems fine? Then no need to convert nested keys to addresses or have more ad hoc encoding.

Copy link
Member Author

Choose a reason for hiding this comment

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

Because we have to deal with non-deterministic encoding of protobuf (i.e. ADR 027). This avoids that. Can you see if what I have their now which does sorting is okay?


## Consequences

### Positive
- a simple algorithm for generating addresses for new public keys

### Negative
- addresses do not communicate key type, a prefixed approach would have done this
Copy link
Member

Choose a reason for hiding this comment

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

Would it be helpful to try and put this info in bech32 encodings? Is access to this info really that important?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know. You're saying that maybe we can have the bech32 have an extra prefix that isn't present in the actual address? Would this be something like cosmossecp256k1sdgh3sghlsdsdg. Or would the prefix get added before bech32 encoding?


### Neutral
- protobuf message names are used as key type prefixes
- public key bytes are hex encoded before generating addresses
aaronc marked this conversation as resolved.
Show resolved Hide resolved

## References
12 changes: 0 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU=
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
github.com/cosmos/iavl v0.15.0-rc1 h1:cYMPAxu5xpGPhGYvGlpeCmgmgH7oY+kebZm2oHfh2nE=
github.com/cosmos/iavl v0.15.0-rc1/go.mod h1:qFTkoCC00sBKWCG3Ws8GAUaYR1jIOtwNZ9p8uFOu4Jo=
github.com/cosmos/iavl v0.15.0-rc2 h1:4HI/LYLjWUnou8dehPD+NqEsDc8uamJOU2yHcqdTKv8=
github.com/cosmos/iavl v0.15.0-rc2/go.mod h1:bXLXbwmww0kYtAYRCYNXR/k44lWaK8rIZJlCmqSK8lQ=
github.com/cosmos/ledger-cosmos-go v0.11.1 h1:9JIYsGnXP613pb2vPjFeMMjBI5lEDsEaF6oYorTy6J4=
Expand Down Expand Up @@ -181,8 +179,6 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -496,7 +492,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
Expand All @@ -523,11 +518,8 @@ github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RM
github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk=
github.com/tendermint/go-amino v0.15.1 h1:D2uk35eT4iTsvJd9jWIetzthE5C0/k2QmMFkCN+4JgQ=
github.com/tendermint/go-amino v0.15.1/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME=
github.com/tendermint/tendermint v0.34.0-rc2/go.mod h1:+AG8ftE2PD4uMVzGSB7c0oH9xbTlIyMeoX0M9r89x3Y=
github.com/tendermint/tendermint v0.34.0-rc3 h1:d7Fsd5rdbxq4GmJ0kRfx7l7LesQM7e70f0ytWLTQ/Go=
github.com/tendermint/tendermint v0.34.0-rc3/go.mod h1:BoHcEpjfpBHc1Be7RQz3AHaXFNObcDG7SNHCev6Or4g=
github.com/tendermint/tm-db v0.6.0 h1:Us30k7H1UDcdqoSPhmP8ztAW/SWV6c6OfsfeCiboTC4=
github.com/tendermint/tm-db v0.6.0/go.mod h1:xj3AWJ08kBDlCHKijnhJ7mTcDMOikT1r8Poxy2pJn7Q=
github.com/tendermint/tm-db v0.6.1 h1:w3X87itMPXopcRPlFiqspEKhw4FXihPk2rnFFkP0zGk=
github.com/tendermint/tm-db v0.6.1/go.mod h1:m3x9kRP4UFd7JODJL0yBAZqE7wTw+S37uAE90cTx7OA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
Expand Down Expand Up @@ -733,8 +725,6 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
Expand Down Expand Up @@ -782,7 +772,5 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=