Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Commit

Permalink
resolve ambiguous prefixes on pub/priv keys and sigs
Browse files Browse the repository at this point in the history
fixes #2416
  • Loading branch information
b1bart committed Apr 16, 2018
1 parent 1a3584e commit 573be19
Show file tree
Hide file tree
Showing 13 changed files with 84 additions and 69 deletions.
24 changes: 15 additions & 9 deletions libraries/fc/include/fc/crypto/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,29 @@ namespace fc { namespace crypto {

template<typename Result, const char * const * Prefixes, int Position, typename KeyType, typename ...Rem>
struct base58_str_parser_impl<Result, Prefixes, Position, KeyType, Rem...> {
static Result apply(const std::string& base58str)
static Result apply(const std::string& prefix_str, const std::string& data_str)
{
using data_type = typename KeyType::data_type;
using wrapper = checksummed_data<data_type>;
constexpr auto prefix = Prefixes[Position];

if (prefix_matches(prefix, base58str)) {
auto str = base58str.substr(const_strlen(prefix));
auto bin = fc::from_base58(str);
if (prefix == prefix_str) {
auto bin = fc::from_base58(data_str);
FC_ASSERT(bin.size() >= sizeof(data_type) + sizeof(uint32_t));
auto wrapped = fc::raw::unpack<wrapper>(bin);
auto checksum = wrapper::calculate_checksum(wrapped.data, prefix);
FC_ASSERT(checksum == wrapped.check);
return Result(KeyType(wrapped.data));
}

return base58_str_parser_impl<Result, Prefixes, Position + 1, Rem...>::apply(base58str);
return base58_str_parser_impl<Result, Prefixes, Position + 1, Rem...>::apply(prefix_str, data_str);
}
};

template<typename Result, const char * const * Prefixes, int Position>
struct base58_str_parser_impl<Result, Prefixes, Position> {
static Result apply(const std::string& base58str) {
FC_ASSERT(false, "No matching prefix for ${str}", ("str", base58str));
static Result apply(const std::string& prefix_str, const std::string& data_str ) {
FC_ASSERT(false, "No matching suite type for ${prefix}.${data}", ("prefix", prefix_str)("data",data_str));
}
};

Expand All @@ -73,7 +72,14 @@ namespace fc { namespace crypto {
template<const char * const * Prefixes, typename ...Ts>
struct base58_str_parser<fc::static_variant<Ts...>, Prefixes> {
static fc::static_variant<Ts...> apply(const std::string& base58str) {
return base58_str_parser_impl<fc::static_variant<Ts...>, Prefixes, 0, Ts...>::apply(base58str);
const auto pivot = base58str.find('.');
FC_ASSERT(pivot != std::string::npos, "No delimiter in data, cannot determine suite type: ${str}", ("str", base58str));

const auto prefix_str = base58str.substr(0, pivot);
auto data_str = base58str.substr(pivot + 1);
FC_ASSERT(!data_str.empty(), "Data only has suite type prefix: ${str}", ("str", base58str));

return base58_str_parser_impl<fc::static_variant<Ts...>, Prefixes, 0, Ts...>::apply(prefix_str, data_str);
}
};

Expand All @@ -91,7 +97,7 @@ namespace fc { namespace crypto {
auto packed = raw::pack( wrapper );
auto data_str = to_base58( packed.data(), packed.size() );
if (!is_default) {
data_str = string(Prefixes[position]) + data_str;
data_str = string(Prefixes[position]) + "." + data_str;
}

return data_str;
Expand Down
2 changes: 1 addition & 1 deletion libraries/fc/include/fc/crypto/private_key.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace fc { namespace crypto {

namespace config {
constexpr const char* private_key_base_prefix = "EOS";
constexpr const char* private_key_base_prefix = "PVT";
constexpr const char* private_key_prefix[] = {
"K1",
"R1"
Expand Down
3 changes: 2 additions & 1 deletion libraries/fc/include/fc/crypto/public_key.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

namespace fc { namespace crypto {
namespace config {
constexpr const char* public_key_base_prefix = "EOS";
constexpr const char* public_key_legacy_prefix = "EOS";
constexpr const char* public_key_base_prefix = "PUB";
constexpr const char* public_key_prefix[] = {
"K1",
"R1"
Expand Down
2 changes: 1 addition & 1 deletion libraries/fc/include/fc/crypto/signature.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace fc { namespace crypto {
namespace config {
constexpr const char* signature_base_prefix = "EOS";
constexpr const char* signature_base_prefix = "SIG";
constexpr const char* signature_prefix[] = {
"K1",
"R1"
Expand Down
23 changes: 14 additions & 9 deletions libraries/fc/src/crypto/private_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,23 @@ namespace fc { namespace crypto {

static private_key::storage_type parse_base58(const string& base58str)
{
try {
if (base58str.find('.') == std::string::npos) {
// wif import
using default_type = private_key::storage_type::template type_at<0>;
return private_key::storage_type(from_wif<default_type>(base58str));
} catch (...) {
// wif import failed
}
} else {
constexpr auto prefix = config::private_key_base_prefix;

const auto pivot = base58str.find('.');
FC_ASSERT(pivot != std::string::npos, "No delimiter in string, cannot determine type: ${str}", ("str", base58str));

constexpr auto prefix = config::private_key_base_prefix;
FC_ASSERT(prefix_matches(prefix, base58str), "Private Key has invalid prefix: ${str}", ("str", base58str));
auto sub_str = base58str.substr(const_strlen(prefix));
return base58_str_parser<private_key::storage_type, config::private_key_prefix>::apply(sub_str);
const auto prefix_str = base58str.substr(0, pivot);
FC_ASSERT(prefix == prefix_str, "Private Key has invalid prefix: ${str}", ("str", base58str)("prefix_str", prefix_str));

auto data_str = base58str.substr(pivot + 1);
FC_ASSERT(!data_str.empty(), "Private Key has no data: ${str}", ("str", base58str));
return base58_str_parser<private_key::storage_type, config::private_key_prefix>::apply(data_str);
}
}

private_key::private_key(const std::string& base58str)
Expand All @@ -118,7 +123,7 @@ namespace fc { namespace crypto {
}

auto data_str = _storage.visit(base58str_visitor<storage_type, config::private_key_prefix>());
return std::string(config::private_key_base_prefix) + data_str;
return std::string(config::private_key_base_prefix) + "." + data_str;
}

std::ostream& operator<<(std::ostream& s, const private_key& k) {
Expand Down
41 changes: 26 additions & 15 deletions libraries/fc/src/crypto/public_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,30 @@ namespace fc { namespace crypto {

static public_key::storage_type parse_base58(const std::string& base58str)
{
constexpr auto prefix = config::public_key_base_prefix;
FC_ASSERT(prefix_matches(prefix, base58str), "Public Key has invalid prefix: ${str}", ("str", base58str));

auto sub_str = base58str.substr(const_strlen(prefix));
try {
constexpr auto legacy_prefix = config::public_key_legacy_prefix;
if(prefix_matches(legacy_prefix, base58str) && base58str.find('.') == std::string::npos ) {
auto sub_str = base58str.substr(const_strlen(legacy_prefix));
using default_type = typename public_key::storage_type::template type_at<0>;
using data_type = default_type::data_type;
using wrapper = checksummed_data<data_type>;
auto bin = fc::from_base58(sub_str);
if (bin.size() == sizeof(data_type) + sizeof(uint32_t)) {
auto wrapped = fc::raw::unpack<wrapper>(bin);
FC_ASSERT(wrapper::calculate_checksum(wrapped.data) == wrapped.check);
return public_key::storage_type(default_type(wrapped.data));
}
} catch (...) {
// default import failed
FC_ASSERT(bin.size() == sizeof(data_type) + sizeof(uint32_t), "");
auto wrapped = fc::raw::unpack<wrapper>(bin);
FC_ASSERT(wrapper::calculate_checksum(wrapped.data) == wrapped.check);
return public_key::storage_type(default_type(wrapped.data));
} else {
constexpr auto prefix = config::public_key_base_prefix;

const auto pivot = base58str.find('.');
FC_ASSERT(pivot != std::string::npos, "No delimiter in string, cannot determine data type: ${str}", ("str", base58str));

const auto prefix_str = base58str.substr(0, pivot);
FC_ASSERT(prefix == prefix_str, "Public Key has invalid prefix: ${str}", ("str", base58str)("prefix_str", prefix_str));

auto data_str = base58str.substr(pivot + 1);
FC_ASSERT(!data_str.empty(), "Public Key has no data: ${str}", ("str", base58str));
return base58_str_parser<public_key::storage_type, config::public_key_prefix>::apply(data_str);
}

return base58_str_parser<public_key::storage_type, config::public_key_prefix>::apply(sub_str);
}

public_key::public_key(const std::string& base58str)
Expand All @@ -54,7 +59,13 @@ namespace fc { namespace crypto {
public_key::operator std::string() const
{
auto data_str = _storage.visit(base58str_visitor<storage_type, config::public_key_prefix, 0>());
return std::string(config::public_key_base_prefix) + data_str;

auto which = _storage.which();
if (which == 0) {
return std::string(config::public_key_legacy_prefix) + data_str;
} else {
return std::string(config::public_key_base_prefix) + "." + data_str;
}
}

std::ostream& operator<<(std::ostream& s, const public_key& k) {
Expand Down
28 changes: 10 additions & 18 deletions libraries/fc/src/crypto/signature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,16 @@ namespace fc { namespace crypto {
static signature::storage_type parse_base58(const std::string& base58str)
{
constexpr auto prefix = config::signature_base_prefix;
FC_ASSERT(prefix_matches(prefix, base58str), "Signature has invalid prefix: ${str}", ("str", base58str));

auto sub_str = base58str.substr(const_strlen(prefix));
try {
using default_type = signature::storage_type::template type_at<0>;
using data_type = default_type::data_type;
using wrapper = checksummed_data<data_type>;
auto bin = fc::from_base58(sub_str);
if (bin.size() == sizeof(data_type) + sizeof(uint32_t)) {
auto wrapped = fc::raw::unpack<wrapper>(bin);
FC_ASSERT(wrapper::calculate_checksum(wrapped.data) == wrapped.check);
return signature::storage_type(default_type(wrapped.data));
}
} catch (...) {
// default import failed
}
const auto pivot = base58str.find('.');
FC_ASSERT(pivot != std::string::npos, "No delimiter in string, cannot determine type: ${str}", ("str", base58str));

const auto prefix_str = base58str.substr(0, pivot);
FC_ASSERT(prefix == prefix_str, "Signature Key has invalid prefix: ${str}", ("str", base58str)("prefix_str", prefix_str));

return base58_str_parser<signature::storage_type, config::signature_prefix>::apply(sub_str);
auto data_str = base58str.substr(pivot + 1);
FC_ASSERT(!data_str.empty(), "Signature has no data: ${str}", ("str", base58str));
return base58_str_parser<signature::storage_type, config::signature_prefix>::apply(data_str);
}

signature::signature(const std::string& base58str)
Expand All @@ -41,8 +33,8 @@ namespace fc { namespace crypto {

signature::operator std::string() const
{
auto data_str = _storage.visit(base58str_visitor<storage_type, config::signature_prefix, 0>());
return std::string(config::signature_base_prefix) + data_str;
auto data_str = _storage.visit(base58str_visitor<storage_type, config::signature_prefix>());
return std::string(config::signature_base_prefix) + "." + data_str;
}

std::ostream& operator<<(std::ostream& s, const signature& k) {
Expand Down
4 changes: 2 additions & 2 deletions libraries/fc/test/crypto/test_cypher_suites.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ BOOST_AUTO_TEST_CASE(test_k1) try {
} FC_LOG_AND_RETHROW();

BOOST_AUTO_TEST_CASE(test_r1) try {
auto private_key_string = std::string("EOSR1iyQmnyPEGvFd8uffnk152WC2WryBjgTrg22fXQryuGL9mU6qW");
auto expected_public_key = std::string("EOSR16EPHFSKVYHBjQgxVGQPrwCxTg7BbZ69H9i4gztN9deKTEXYne4");
auto private_key_string = std::string("PVT.R1.iyQmnyPEGvFd8uffnk152WC2WryBjgTrg22fXQryuGL9mU6qW");
auto expected_public_key = std::string("PUB.R1.6EPHFSKVYHBjQgxVGQPrwCxTg7BbZ69H9i4gztN9deKTEXYne4");
auto test_private_key = private_key(private_key_string);
auto test_public_key = test_private_key.get_public_key();

Expand Down
2 changes: 1 addition & 1 deletion programs/cleos/help_text.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ const char* error_advice_3120010 = R"=====(Ensure that your packed transaction
}
e.g.
{
"signatures" : [ "EOSJze4m1ZHQ4UjuHpBcX6uHPN4Xyggv52raQMTBZJghzDLepaPcSGCNYTxaP2NiaF4yRF5RaYwqsQYAwBwFtfuTJr34Z5GJX" ],
"signatures" : [ "SIG.K1.Jze4m1ZHQ4UjuHpBcX6uHPN4Xyggv52raQMTBZJghzDLepaPcSGCNYTxaP2NiaF4yRF5RaYwqsQYAwBwFtfuTJr34Z5GJX" ],
"compression" : "none",
"hex_transaction" : "6c36a25a00002602626c5e7f0000000000010000001e4d75af460000000000a53176010000000000ea305500000000a8ed3232180000001e4d75af4680969800000000000443555200000000"
})=====";
Expand Down
8 changes: 4 additions & 4 deletions programs/eosio-applesedemo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ that requires usage of a fingerprint when signing a digest (such a signing a tra
```
$ applesedemo.app/Contents/MacOS/applesedemo --create-se-touch-only
Successfully created
public_key(EOSR17iKCZrb1JqSjUncCbDGQenzSqyuNmPU8iUA15efNW5KD1iHd9x)
public_key(PUB.R1.7iKCZrb1JqSjUncCbDGQenzSqyuNmPU8iUA15efNW5KD1iHd9x)
```
The public key for the private key is printed; note it somewhere. It is possible to ask the Secure Enclave to
create multiple private keys and assign labels to them but this application only allows a single key at once.
Expand All @@ -64,14 +64,14 @@ Now, ask the Secure Enclave to sign the digest with the private key only it know
generated until a valid fingerprint is used.
```
$ applesedemo.app/Contents/MacOS/applesedemo --sign 0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8
signature(EOSR1Jx4sBidhFV6PSvS8hWbG5oh77HKud8xpkoHLvWaZVaBeWttRpyEjaGbPRVEKu3JePTyVjANmP4GKFtG2DAuB4MTVqsdC9W)
signature(SIG.R1.Jx4sBidhFV6PSvS8hWbG5oh77HKud8xpkoHLvWaZVaBeWttRpyEjaGbPRVEKu3JePTyVjANmP4GKFtG2DAuB4MTVqsdC9W)
```
## Key Recovery
Given the signature and digest, we must be able to correlate that with the public key that signed the digest.
```
$ applesedemo.app/Contents/MacOS/applesedemo --recover 0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8 \
EOSR1Jx4sBidhFV6PSvS8hWbG5oh77HKud8xpkoHLvWaZVaBeWttRpyEjaGbPRVEKu3JePTyVjANmP4GKFtG2DAuB4MTVqsdC9W
public_key(EOSR17iKCZrb1JqSjUncCbDGQenzSqyuNmPU8iUA15efNW5KD1iHd9x)
SIG.R1.Jx4sBidhFV6PSvS8hWbG5oh77HKud8xpkoHLvWaZVaBeWttRpyEjaGbPRVEKu3JePTyVjANmP4GKFtG2DAuB4MTVqsdC9W
public_key(PUB.R1.7iKCZrb1JqSjUncCbDGQenzSqyuNmPU8iUA15efNW5KD1iHd9x)
```
Indeed, the public key recovered from this digest and signature matches the public key from the key stored in the Secure
Enclave.
4 changes: 2 additions & 2 deletions programs/eosio-applesedemo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ void print_pub_for_key(SecKeyRef key) {
pub_wrapper.check = fc::crypto::checksummed_data<fc::crypto::r1::public_key_data>::calculate_checksum(pub_wrapper.data, "R1");
std::vector<char> checksummed = fc::raw::pack(pub_wrapper);

cout << "public_key(EOSR1" << fc::to_base58(checksummed.data(), checksummed.size()) << ")" << endl;
cout << "public_key(PUB.R1." << fc::to_base58(checksummed.data(), checksummed.size()) << ")" << endl;
}

void print_attributes() {
Expand Down Expand Up @@ -221,7 +221,7 @@ void sign(const string& hex) {
signature_wrapper.check = fc::crypto::checksummed_data<fc::crypto::r1::compact_signature>::calculate_checksum(signature_wrapper.data, "R1");
checksummed = fc::raw::pack(signature_wrapper);

cout << "signature(EOSR1" << fc::to_base58(checksummed.data(), checksummed.size()) << ")" << endl;
cout << "signature(SIG.R1." << fc::to_base58(checksummed.data(), checksummed.size()) << ")" << endl;

err:
CFRelease(key);
Expand Down
10 changes: 5 additions & 5 deletions tests/tests/abi_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1801,8 +1801,8 @@ BOOST_AUTO_TEST_CASE(general)
"string_arr" : ["ola ke ase","ola ke desi"],
"time" : "2021-12-20T15:30",
"time_arr" : ["2021-12-20T15:30","2021-12-20T15:31"],
"signature" : "EOSJzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwT77X1U",
"signature_arr" : ["EOSJzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwT77X1U","EOSJzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwT77X1U"],
"signature" : "SIG.K1.Jzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwV1v4G5",
"signature_arr" : ["SIG.K1.Jzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwV1v4G5","SIG.K1.Jzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwV1v4G5"],
"checksum256" : "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"checksum256_arr" : ["ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
"fieldname" : "name1",
Expand Down Expand Up @@ -1894,7 +1894,7 @@ BOOST_AUTO_TEST_CASE(general)
"ref_block_prefix":"2",
"expiration":"2021-12-20T15:30",
"region": "1",
"signatures" : ["EOSJzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwT77X1U"],
"signatures" : ["SIG.K1.Jzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwV1v4G5"],
"context_free_data" : ["abcdef","0123456789","ABCDEF0123456789abcdef"],
"context_free_actions":[{"account":"contextfree1", "name":"cfactionname1", "authorization":[{"actor":"cfacc1","permission":"cfpermname1"}], "data":"778899"}],
"actions":[{"account":"accountname1", "name":"actionname1", "authorization":[{"actor":"acc1","permission":"permname1"}], "data":"445566"}],
Expand All @@ -1907,7 +1907,7 @@ BOOST_AUTO_TEST_CASE(general)
"ref_block_prefix":"2",
"expiration":"2021-12-20T15:30",
"region": "1",
"signatures" : ["EOSJzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwT77X1U"],
"signatures" : ["SIG.K1.Jzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwV1v4G5"],
"context_free_data" : ["abcdef","0123456789","ABCDEF0123456789abcdef"],
"context_free_actions":[{"account":"contextfree1", "name":"cfactionname1", "authorization":[{"actor":"cfacc1","permission":"cfpermname1"}], "data":"778899"}],
"actions":[{"account":"acc1", "name":"actionname1", "authorization":[{"actor":"acc1","permission":"permname1"}], "data":"445566"}],
Expand All @@ -1919,7 +1919,7 @@ BOOST_AUTO_TEST_CASE(general)
"ref_block_prefix":"3",
"expiration":"2021-12-20T15:40",
"region": "1",
"signatures" : ["EOSJzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwT77X1U"],
"signatures" : ["SIG.K1.Jzdpi5RCzHLGsQbpGhndXBzcFs8vT5LHAtWLMxPzBdwRHSmJkcCdVu6oqPUQn1hbGUdErHvxtdSTS1YA73BThQFwV1v4G5"],
"context_free_data" : ["abcdef","0123456789","ABCDEF0123456789abcdef"],
"context_free_actions":[{"account":"contextfree2", "name":"cfactionname2", "authorization":[{"actor":"cfacc2","permission":"cfpermname2"}], "data":"667788"}],
"actions":[{"account":"acc2", "name":"actionname2", "authorization":[{"actor":"acc2","permission":"permname2"}], "data":""}],
Expand Down
2 changes: 1 addition & 1 deletion tests/wasm_tests/eosio.system_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ BOOST_FIXTURE_TEST_CASE( producer_register_unregister, eosio_system_tester ) try
//call regproducer again to change parameters
fc::variant params2 = producer_parameters_example(2);

vector<char> key2 = fc::raw::pack( fc::crypto::public_key( std::string("EOSR16EPHFSKVYHBjQgxVGQPrwCxTg7BbZ69H9i4gztN9deKTEXYne4") ) );
vector<char> key2 = fc::raw::pack( fc::crypto::public_key( std::string("PUB.R1.6EPHFSKVYHBjQgxVGQPrwCxTg7BbZ69H9i4gztN9deKTEXYne4") ) );
BOOST_REQUIRE_EQUAL( success(), push_action(N(alice), N(regproducer), mvo()
("producer", "alice")
("producer_key", key2 )
Expand Down

0 comments on commit 573be19

Please sign in to comment.