diff --git a/.gitmodules b/.gitmodules index 005915f84..3eca02b9a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "vendor/sqlite3-unicodesn"] path = vendor/sqlite3-unicodesn url = https://github.com/couchbasedeps/sqlite3-unicodesn +[submodule "vendor/Monocypher"] + path = vendor/Monocypher + url = https://github.com/LoupVaillant/Monocypher.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 57da20561..5b219bd91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,6 +286,8 @@ target_include_directories( vendor/sqlite3-unicodesn vendor/mbedtls/include vendor/mbedtls/crypto/include + vendor/Monocypher/src + vendor/Monocypher/src/optional vendor/sockpp/include ) diff --git a/Crypto/SecureDigest.cc b/Crypto/SecureDigest.cc index 18b6b1834..b54d34ee8 100644 --- a/Crypto/SecureDigest.cc +++ b/Crypto/SecureDigest.cc @@ -17,69 +17,117 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation-deprecated-sync" #include "mbedtls/sha1.h" +#include "mbedtls/sha256.h" #pragma clang diagnostic pop #ifdef __APPLE__ -#define USE_COMMON_CRYPTO +# define USE_COMMON_CRYPTO #endif #ifdef USE_COMMON_CRYPTO #include - #define _CONTEXT ((CC_SHA1_CTX*)_context) -#else - #define _CONTEXT ((mbedtls_sha1_context*)_context) #endif namespace litecore { - void SHA1::computeFrom(fleece::slice s) { - (SHA1Builder() << s).finish(&bytes, sizeof(bytes)); - } - - bool SHA1::setDigest(fleece::slice s) { - if (s.size != sizeof(bytes)) + template + bool Digest::setDigest(fleece::slice s) { + if (s.size != _bytes.size()) return false; - memcpy(bytes, s.buf, sizeof(bytes)); + s.copyTo(_bytes.data()); return true; } - std::string SHA1::asBase64() const { + template + std::string Digest::asBase64() const { return fleece::base64::encode(asSlice()); } - SHA1Builder::SHA1Builder() { +#pragma mark - SHA1: + + + template <> + Digest::Builder::Builder() { static_assert(sizeof(_context) >= sizeof(mbedtls_sha1_context)); #ifdef USE_COMMON_CRYPTO static_assert(sizeof(_context) >= sizeof(CC_SHA1_CTX)); - CC_SHA1_Init(_CONTEXT); + CC_SHA1_Init((CC_SHA1_CTX*)_context); #else - mbedtls_sha1_init(_CONTEXT); - mbedtls_sha1_starts(_CONTEXT); + mbedtls_sha1_init((mbedtls_sha1_context*)_context); + mbedtls_sha1_starts((mbedtls_sha1_context*)_context); #endif } - SHA1Builder& SHA1Builder::operator<< (fleece::slice s) { + template <> + Digest::Builder& Digest::Builder::operator<< (fleece::slice s) { #ifdef USE_COMMON_CRYPTO - CC_SHA1_Update(_CONTEXT, s.buf, (CC_LONG)s.size); + CC_SHA1_Update((CC_SHA1_CTX*)_context, s.buf, (CC_LONG)s.size); #else - mbedtls_sha1_update(_CONTEXT, (unsigned char*)s.buf, s.size); + mbedtls_sha1_update((mbedtls_sha1_context*)_context, (unsigned char*)s.buf, s.size); #endif return *this; } - void SHA1Builder::finish(void *result, size_t resultSize) { - DebugAssert(resultSize == sizeof(SHA1::bytes)); + template <> + void Digest::Builder::finish(void *result, size_t resultSize) { + Assert(resultSize == kSizeInBytes); #ifdef USE_COMMON_CRYPTO - CC_SHA1_Final((uint8_t*)result, _CONTEXT); + CC_SHA1_Final((uint8_t*)result, (CC_SHA1_CTX*)_context); #else - mbedtls_sha1_finish(_CONTEXT, (uint8_t*)result); - mbedtls_sha1_free(_CONTEXT); + mbedtls_sha1_finish((mbedtls_sha1_context*)_context, (uint8_t*)result); + mbedtls_sha1_free((mbedtls_sha1_context*)_context); #endif } + // Force the non-specialized methods to be instantiated: + template class Digest; + + +#pragma mark - SHA256: + + + template <> + Digest::Builder::Builder() { + static_assert(sizeof(_context) >= sizeof(mbedtls_sha256_context)); +#ifdef USE_COMMON_CRYPTO + static_assert(sizeof(_context) >= sizeof(CC_SHA256_CTX)); + CC_SHA256_Init((CC_SHA256_CTX*)_context); +#else + mbedtls_sha256_init((mbedtls_sha256_context*)_context); + mbedtls_sha256_starts((mbedtls_sha256_context*)_context, 0); +#endif + } + + + template <> + Digest::Builder& Digest::Builder::operator<< (fleece::slice s) { +#ifdef USE_COMMON_CRYPTO + CC_SHA256_Update((CC_SHA256_CTX*)_context, s.buf, (CC_LONG)s.size); +#else + mbedtls_sha256_update((mbedtls_sha256_context*)_context, (unsigned char*)s.buf, s.size); +#endif + return *this; + } + + + template <> + void Digest::Builder::finish(void *result, size_t resultSize) { + Assert(resultSize == kSizeInBytes); +#ifdef USE_COMMON_CRYPTO + CC_SHA256_Final((uint8_t*)result, (CC_SHA256_CTX*)_context); +#else + mbedtls_sha256_finish((mbedtls_sha256_context*)_context, (uint8_t*)result); + mbedtls_sha256_free((mbedtls_sha256_context*)_context); +#endif + } + + + // Force the non-specialized methods to be instantiated: + template class Digest; + } diff --git a/Crypto/SecureDigest.hh b/Crypto/SecureDigest.hh index 085a4c6de..edeac39ba 100644 --- a/Crypto/SecureDigest.hh +++ b/Crypto/SecureDigest.hh @@ -12,66 +12,96 @@ #pragma once #include "fleece/slice.hh" +#include #include namespace litecore { - /// A SHA-1 digest. - class SHA1 { + enum DigestType { + SHA, + }; + + + /// A cryptographic digest. Available instantiations are and . + /// (SHA384 and SHA512 could be added by doing some copy-and-pasting in the .cc file.) + template + class Digest { public: - SHA1() { memset(bytes, 0, sizeof(bytes)); } + class Builder; + + Digest() {_bytes.fill(std::byte{0});} + + /// Constructs instance with a digest of the data in `s`. + explicit Digest(fleece::slice s) {computeFrom(s);} - /// Constructs instance with a SHA-1 digest of the data in `s` - explicit SHA1(fleece::slice s) {computeFrom(s);} + inline Digest(Builder&&); - /// Computes a SHA-1 digest of the data - void computeFrom(fleece::slice); + /// Computes a digest of the data. + void computeFrom(fleece::slice data); - /// Stores a digest; returns false if slice is the wrong size - bool setDigest(fleece::slice); + /// Stores a digest; returns false if slice is the wrong size. + bool setDigest(fleece::slice digestData); - /// The digest as a slice - fleece::slice asSlice() const {return {bytes, sizeof(bytes)};} + /// The digest as a slice. + fleece::slice asSlice() const {return {_bytes.data(), _bytes.size()};} operator fleece::slice() const {return asSlice();} + /// The digest encoded in Base64. std::string asBase64() const; - bool operator==(const SHA1 &x) const {return memcmp(&bytes, &x.bytes, sizeof(bytes)) == 0;} - bool operator!= (const SHA1 &x) const {return !(*this == x);} + bool operator==(const Digest &x) const {return _bytes == x._bytes;} + bool operator!=(const Digest &x) const {return _bytes != x._bytes;} private: - char bytes[20]; + static constexpr size_t kSizeInBytes = ((TYPE == SHA && SIZE == 1) ? 160 : SIZE) / 8; - friend class SHA1Builder; + std::array _bytes; }; - /// Builder for creating SHA-1 digests from piece-by-piece data. - class SHA1Builder { + /// Builder for creating digests incrementally from piece-by-piece data. + template + class Digest::Builder { public: - SHA1Builder(); + Builder(); - /// Add a single byte - SHA1Builder& operator<< (fleece::slice s); + /// Adds data. + Builder& operator<< (fleece::slice s); - /// Add data - SHA1Builder& operator<< (uint8_t b) {return *this << fleece::slice(&b, 1);} + /// Adds a single byte. + Builder& operator<< (uint8_t b) {return *this << fleece::slice(&b, 1);} - /// Finish and write the digest to `result`. (Don't reuse the builder.) + /// Finishes and writes the digest. + /// @warning Don't reuse this builder. + /// @param result The address to write the digest to. + /// @param resultSize Must be equal to the size of a digest. void finish(void *result, size_t resultSize); - /// Finish and return the digest as a SHA1 object. (Don't reuse the builder.) - SHA1 finish() { - SHA1 result; - finish(&result.bytes, sizeof(result.bytes)); - return result; - } + /// Finishes and returns the digest as a new object. + /// @warning Don't reuse this builder. + Digest finish() {return Digest(std::move(*this));} private: - uint8_t _context[100]; // big enough to hold any platform's context struct + std::byte _context[110]; // big enough to hold any platform's context struct }; + template + Digest::Digest(Builder &&builder) { + builder.finish(_bytes.data(), _bytes.size()); + } + + template + void Digest::computeFrom(fleece::slice s) { + (Builder() << s).finish(_bytes.data(), _bytes.size()); + } + + + // Shorthand names: + using SHA1 = Digest; + using SHA1Builder = Digest::Builder; + + using SHA256 = Digest; } diff --git a/Crypto/SignatureTest.cc b/Crypto/SignatureTest.cc new file mode 100644 index 000000000..55bdfb2cb --- /dev/null +++ b/Crypto/SignatureTest.cc @@ -0,0 +1,114 @@ +// +// SignatureTest.cc +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "PublicKey.hh" +#include "SignedDict.hh" +#include "Base64.hh" +#include "Error.hh" +#include "LiteCoreTest.hh" +#include "fleece/Mutable.hh" +#include + + +using namespace litecore; +using namespace litecore::crypto; +using namespace std; +using namespace fleece; + + +TEST_CASE("Signatures", "[Signatures]") { + static constexpr slice kDataToSign = "The only thing we learn from history" + " is that people do not learn from history. --Hegel"; + + const char *alg = GENERATE(kRSAAlgorithmName, kEd25519AlgorithmName); + cerr << "\t---- " << alg << endl; + + auto signingKey = SigningKey::generate(alg); + alloc_slice signature = signingKey->sign(kDataToSign); + cout << "Signature is " << signature.size << " bytes: " << base64::encode(signature) << endl; + + // Verify: + auto verifyingKey = signingKey->verifyingKey(); + CHECK(verifyingKey->verifySignature(kDataToSign, signature)); + + // Verification fails with wrong public key: + auto key2 = SigningKey::generate(kRSAAlgorithmName); + CHECK(!key2->verifyingKey()->verifySignature(kDataToSign, signature)); + + // Verification fails with incorrect digest: + auto badDigest = SHA256(kDataToSign); + ((uint8_t*)&badDigest)[10]++; + CHECK(!verifyingKey->verifySignature(badDigest, signature)); + + // Verification fails with altered signature: + ((uint8_t&)signature[30])++; + CHECK(!verifyingKey->verifySignature(kDataToSign, signature)); +} + + +TEST_CASE("Signed Document", "[Signatures]") { + const char *algorithm = GENERATE(kRSAAlgorithmName, kEd25519AlgorithmName); + bool embedKey = GENERATE(false, true); + cerr << "\t---- " << algorithm << "; embed key in signature = " << embedKey << endl; + + // Create a signed doc and convert to JSON: + alloc_slice publicKeyData; + string json; + { + auto priv = SigningKey::generate(algorithm); + auto pub = priv->verifyingKey(); + publicKeyData = pub->data(); + + MutableDict doc = MutableDict::newDict(); + doc["name"] = "Oliver Bolliver Butz"; + doc["age"] = 6; + cout << "Document: " << doc.toJSONString() << endl; + + MutableDict sig = makeSignature(doc, *priv, 5 /*minutes*/, embedKey); + REQUIRE(sig); + string sigJson = sig.toJSONString(); + cout << "Signature, " << sigJson.size() << " bytes: " << sigJson << endl; + + CHECK(verifySignature(doc, sig, pub.get()) == VerifyResult::Valid); + + doc["(sig)"] = sig; // <-- add signature to doc, in "(sig)" property + json = doc.toJSONString(); + } + cout << "Signed Document: " << json << endl; + + // Now parse the JSON and verify the signature: + { + Doc parsedDoc = Doc::fromJSON(json); + Dict doc = parsedDoc.asDict(); + Dict sig = doc["(sig)"].asDict(); + REQUIRE(sig); + + auto parsedKey = getSignaturePublicKey(sig, algorithm); + if (embedKey) { + REQUIRE(parsedKey); + CHECK(parsedKey->data() == publicKeyData); + } else { + CHECK(!parsedKey); + parsedKey = VerifyingKey::instantiate(publicKeyData, algorithm); + } + + MutableDict unsignedDoc = doc.mutableCopy(); + unsignedDoc.remove("(sig)"); // <-- detach signature to restore doc to signed form + + if (embedKey) + CHECK(verifySignature(unsignedDoc, sig) == VerifyResult::Valid); + else + CHECK(verifySignature(unsignedDoc, sig) == VerifyResult::MissingKey); + + CHECK(verifySignature(unsignedDoc, sig, parsedKey.get()) == VerifyResult::Valid); + } +} diff --git a/Crypto/SignedDict.cc b/Crypto/SignedDict.cc new file mode 100644 index 000000000..51d3bcd06 --- /dev/null +++ b/Crypto/SignedDict.cc @@ -0,0 +1,188 @@ +// +// SignedDict.cc +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "SignedDict.hh" +#include "SecureDigest.hh" +#include "Error.hh" +#include "fleece/Mutable.hh" +#include "Base64.hh" + +namespace litecore::crypto { + using namespace std::string_literals; + using namespace fleece; + + /* + Signature dict schema: + { + "sig_RSA" + or "sig_Ed25519": A digital signature of the canonical JSON form of this signature + dict itself. (When verifying, this property must be removed + since it didn't exist when the signature was being computed.) + The suffix after "sig_" is the value of `SigningKey::algorithmName()`. + "digest_SHA": A SHA digest of the canonical JSON of the value being signed. + Usually SHA256; the specific algorithm can be determined by the data's size. + "key": The [optional] public key data for verifying the signature. + The algorithm is the same as indicated by the "sig_..." property's suffix. + If not present, the verifier must know the key through some other means + and pass it to `verifySignature()`. + "date": A timestamp of when the signature was created. + "expires": The number of minutes before the signature expires. + } + + Other optional application-defined properties may be added to the signature dict. + They become part of the signature, so they cannot be tampered with, + but the signature verification code here doesn't pay any attention to them. + + - Data is either a base64-encoded string, or a Fleece data value. + - A timestamp is either a number of milliseconds since the Unix epoch, or an ISO-8601 string. + - Canonical JSON rules: + * No whitespace. + * Dicts are ordered by sorting the keys lexicographically (before encoding them as JSON.) + * Strings use only the escape sequences `\\`, `\"`, `\r`, `\n`, `\t`, and the generic + escape sequence `\uxxxx` for other control characters and 0x7F. All others are literal, + including non-ASCII UTF-8 sequences. + * No leading zeroes in integers, and no `-` in front of `0`. + * Floating-point numbers should be avoided since there's no universally recognized algorithm + to convert them to decimal, so different encoders may produce different results. + */ + + + // The amount by which a signature's start date may be in the future and still be considered + // valid when verifying it. + // This compensates for clock inconsistency between computers: if you create a signature and + // immediately send it over the network to someone else, but their system clock is slightly + // behind yours, they will probably see the signature's date as being in the future. Without + // some allowance for this, they'd reject the signature. + // In other words, this is the maximum clock variance we allow when verifying a just-created + // signature. + static constexpr int64_t kClockDriftAllowanceMS = 60 * 1000; + + + MutableDict makeSignature(Value toBeSigned, + const SigningKey &privateKey, + int64_t expirationTimeMinutes, + bool embedPublicKey, + Dict otherMetadata) + { + // Create a signature object containing the document digest and the public key: + MutableDict signature = otherMetadata ? otherMetadata.mutableCopy() : MutableDict::newDict(); + SHA256 digest(toBeSigned.toJSON(false, true)); + signature["digest_SHA"].setData(digest); + if (embedPublicKey) + signature["key"].setData(privateKey.verifyingKeyData()); + if (expirationTimeMinutes > 0) { + if (!signature["date"]) + signature["date"] = FLTimestamp_Now();// alloc_slice(FLTimestamp_ToString(FLTimestamp_Now(), false)); + if (!signature["expires"]) + signature["expires"] = expirationTimeMinutes; + } + + // Sign the signature object, add the signature, and return it: + alloc_slice signatureData = privateKey.sign(signature.toJSON(false, true)); + signature["sig_"s + privateKey.algorithmName()].setData(signatureData); + return signature; + } + + + static alloc_slice convertToData(Value dataOrStr) { + if (slice data = dataOrStr.asData(); data) + return alloc_slice(data); + else if (slice str = dataOrStr.asString(); str) + return base64::decode(str); + else + return nullslice; + } + + + unique_ptr getSignaturePublicKey(Dict signature, const char *algorithmName) { + alloc_slice data = convertToData(signature["key"]); + if (!data) + return nullptr; + if (!signature["sig_"s + algorithmName]) + return nullptr; + return VerifyingKey::instantiate(data, algorithmName); + } + + + unique_ptr getSignaturePublicKey(Dict signature) { + auto key = getSignaturePublicKey(signature, kRSAAlgorithmName); + if (!key) + key = getSignaturePublicKey(signature, kEd25519AlgorithmName); + return key; + } + + + VerifyResult verifySignature(Value toBeVerified, + Dict signature, + const VerifyingKey *publicKey) + { + // Get the digest property from the signature: + Value digestVal = signature["digest_SHA"]; + if (!digestVal) + return VerifyResult::InvalidProperties; + auto digest = convertToData(digestVal); + if (!digest || digest.size != sizeof(SHA256)) + return VerifyResult::InvalidProperties; + + unique_ptr embeddedKey; + if (publicKey) { + // If there's an embedded key, make sure it matches the key I was given: + if (Value key = signature["key"]; key && convertToData(key) != publicKey->data()) + return VerifyResult::ConflictingKeys; + } else { + // If no public key was given, read it from the signature: + embeddedKey = getSignaturePublicKey(signature); + if (!embeddedKey) + return VerifyResult::MissingKey; + publicKey = embeddedKey.get(); + } + + // Find the signature data itself: + string sigProp = "sig_"s + publicKey->algorithmName(); + auto signatureData = convertToData(signature[sigProp]); + if (!signatureData) + return VerifyResult::InvalidProperties; + + // Generate canonical JSON of the signature dict, minus the "sig_" property: + MutableDict strippedSignature = signature.mutableCopy(); + strippedSignature.remove(sigProp); + alloc_slice signedData = strippedSignature.toJSON(false, true); + + // Verify the signature: + if (!publicKey->verifySignature(signedData, signatureData)) + return VerifyResult::InvalidSignature; + + // Verify that the digest matches that of the document: + if (digest != SHA256(toBeVerified.toJSON(false, true)).asSlice()) + return VerifyResult::InvalidDigest; + + // Verify that the signature is not expired nor not-yet-valid: + if (Value date = signature["date"]; date) { + FLTimestamp now = FLTimestamp_Now(); + FLTimestamp start = date.asTimestamp(); + if (start <= 0) + return VerifyResult::InvalidProperties; + if (now + kClockDriftAllowanceMS < start) + return VerifyResult::Expired; + if (Value exp = signature["expires"]; exp) { + int64_t expMinutes = exp.asInt(); + if (expMinutes <= 0) + return VerifyResult::InvalidProperties; + if ((now - start) / 60000 > expMinutes) + return VerifyResult::Expired; + } + } + + return VerifyResult::Valid; + } + +} diff --git a/Crypto/SignedDict.hh b/Crypto/SignedDict.hh new file mode 100644 index 000000000..1fbeade53 --- /dev/null +++ b/Crypto/SignedDict.hh @@ -0,0 +1,83 @@ +// +// SignedDict.hh +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#pragma once +#include "Base.hh" +#include "Signing.hh" +#include "fleece/Fleece.hh" + +namespace litecore::crypto { + + /// Possible results of verifying a signature. + /// Any result other than `Valid` means the signature is not valid and the contents of the + /// object are not to be trusted. The specific values might help in choosing an error message. + enum class VerifyResult { + Valid, ///< The signature is valid! + Expired, ///< The signature was valid but has expired (or isn't valid yet.) + MissingKey, ///< No key was given and there's no key embedded in the signature. + ConflictingKeys, ///< Key given doesn't match public key embedded in signature. + InvalidProperties, ///< Properties in the signature dict are missing or invalid. + InvalidDigest, ///< Digest in signature doesn't match that of the signed object itself. + InvalidSignature ///< The signature data itself didn't check out. + }; + + + /// Creates a signature of a Fleece Value, usually a Dict. + /// The signature takes the form of a Dict. + /// @param toBeSigned The Fleece value, usually a Dict, to be signed. + /// @param key A private key to sign with, RSA or Ed25519. + /// @param expirationTimeMinutes How long until the signature expires. Units are **minutes**. + /// Default value is one year. + /// @param embedPublicKey If true, the public key data will be included in the signature object. + /// If false it's omitted; then whoever verifies the signature + /// must already know the public key through some other means. + /// @param otherMetadata An optional Dict of other properties to add to the signature Dict. + /// These properties will be signed, so any tampering of them will + /// invalidate the signature just like tampering with `toBeSigned`. + /// @return The signature object, a (mutable) Dict. + [[nodiscard]] + fleece::MutableDict makeSignature(fleece::Value toBeSigned, + const SigningKey &key, + int64_t expirationTimeMinutes = 60 * 24 * 365, + bool embedPublicKey = true, + fleece::Dict otherMetadata =nullptr); + + + /// Returns the public key embedded in a signature, if there is one. + /// Returns `nullptr` if the signature has no key data for any known algorithm. + /// Throws `error::CryptoError` if the key data exists but is invalid. + unique_ptr getSignaturePublicKey(fleece::Dict signature); + + + /// Returns the public key, with the given algorithm, embedded in a signature. + /// Returns `nullptr` if the signature has no key data for that algorithm. + /// Throws `error::CryptoError` if the key data exists but is invalid. + unique_ptr getSignaturePublicKey(fleece::Dict signature, + const char *algorithmName); + + + /// Verifies a signature of `document` using the signature object `signature`. + /// The `document` must be _exactly the same_ as when it was signed; any properties added to it + /// afterwards need to be removed. This probably includes the `signature` itself! + /// @param toBeVerified The Fleece value which is to be verified. + /// @param signature The signature. (Must not be contained in `toBeVerified`!) + /// @param publicKey The `VerifyingKey` matching the `SigningKey` that made the signature. + /// If `nullptr`, a key embedded in the signature will be used. + /// @return An status value, which will be `Valid` if the signature is valid; + /// or `MissingDigest` or `MissingKey` if no digest or key properties corresponding to + /// the verifier were found; + /// or other values if the signature itself is invalid or expired. + [[nodiscard]] + VerifyResult verifySignature(fleece::Value toBeVerified, + fleece::Dict signature, + const VerifyingKey *publicKey =nullptr); +} diff --git a/Crypto/Signing.cc b/Crypto/Signing.cc new file mode 100644 index 000000000..57b40407b --- /dev/null +++ b/Crypto/Signing.cc @@ -0,0 +1,155 @@ +// +// Signing.cc +// +// Copyright © 2022 Couchbase. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "Signing.hh" +#include "Error.hh" +#include "SecureRandomize.hh" +#include "mbedUtils.hh" +#include "monocypher.h" +#include "monocypher-ed25519.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation-deprecated-sync" +#include "mbedtls/pk.h" +#pragma clang diagnostic pop + +namespace litecore::crypto { + using namespace std; + + + std::unique_ptr SigningKey::generate(const char *algorithm) { + if (0 == strcmp(algorithm, kRSAAlgorithmName)) { + return make_unique(PrivateKey::generateTemporaryRSA(2048)); + } else if (0 == strcmp(algorithm, kEd25519AlgorithmName)) { + return make_unique(); + } else { + error::_throw(error::CryptoError, "Unknown signature algorithm '%s'", algorithm); + } + } + + + unique_ptr VerifyingKey::instantiate(slice data, const char *algorithm) { + if (0 == strcmp(algorithm, kRSAAlgorithmName)) { + return make_unique(data); + } else if (0 == strcmp(algorithm, kEd25519AlgorithmName)) { + return make_unique(data); + } else { + error::_throw(error::CryptoError, "Unknown signature algorithm '%s'", algorithm); + } + } + + +#pragma mark - RSA: + + + static int rngFunction(void *ctx, unsigned char *dst, size_t size) { + SecureRandomize({dst, size}); + return 0; + } + + + alloc_slice RSASigningKey::sign(slice data) const { + SHA256 inputDigest(data); + alloc_slice signature(MBEDTLS_PK_SIGNATURE_MAX_SIZE); + size_t sigLen = 0; + TRY(mbedtls_pk_sign(_key->context(), + MBEDTLS_MD_SHA256, // declares that input is a SHA256 digest. + (const uint8_t*)inputDigest.asSlice().buf, inputDigest.asSlice().size, + (uint8_t*)signature.buf, &sigLen, + rngFunction, nullptr)); + signature.shorten(sigLen); + return signature; + } + + + unique_ptr RSASigningKey::verifyingKey() const { + return make_unique(_key->publicKey()); + } + + + bool RSAVerifyingKey::verifySignature(slice data, slice signature) const { + SHA256 inputDigest(data); + int result = mbedtls_pk_verify(_key->context(), + MBEDTLS_MD_SHA256, // declares that input is a SHA256 digest. + (const uint8_t*)inputDigest.asSlice().buf, + inputDigest.asSlice().size, + (const uint8_t*)signature.buf, signature.size); + if (result == MBEDTLS_ERR_RSA_VERIFY_FAILED) + return false; + TRY(result); // other error codes throw exceptions + return true; + } + + +#pragma mark - Ed25519: + + + Ed25519Base::Ed25519Base(slice bytes) { + if (bytes.size != sizeof(_bytes)) + error::_throw(error::CryptoError, "Invalid data size for Ed25519 key"); + bytes.copyTo(_bytes.data()); + } + + + Ed25519SigningKey::Ed25519SigningKey() { + SecureRandomize({_bytes.data(), _bytes.size()}); + } + + + Ed25519VerifyingKey Ed25519SigningKey::publicKey() const{ + Ed25519VerifyingKey pub; + crypto_ed25519_public_key(pub._bytes.data(), _bytes.data()); + return pub; + } + + + unique_ptr Ed25519SigningKey::verifyingKey() const { + unique_ptr pub(new Ed25519VerifyingKey()); + crypto_ed25519_public_key(pub->_bytes.data(), _bytes.data()); + return pub; + } + + + alloc_slice Ed25519SigningKey::verifyingKeyData() const { + return publicKey().data(); + } + + + alloc_slice Ed25519SigningKey::sign(slice data) const { + alloc_slice signature(kSignatureSize); + crypto_ed25519_sign((uint8_t*)signature.buf, + (const uint8_t*)_bytes.data(), nullptr, + (const uint8_t*)data.buf, data.size); + return signature; + } + + + Ed25519VerifyingKey::Ed25519VerifyingKey(slice bytes) { + Assert(bytes.size == sizeof(_bytes)); + bytes.copyTo(_bytes.data()); + } + + bool Ed25519VerifyingKey::verifySignature(slice inputData, slice signature) const { + return signature.size == kSignatureSize + && 0 == crypto_ed25519_check((const uint8_t*)signature.buf, + (const uint8_t*)_bytes.data(), + (const uint8_t*)inputData.buf, + inputData.size); + } + +} diff --git a/Crypto/Signing.hh b/Crypto/Signing.hh new file mode 100644 index 000000000..51ce06c07 --- /dev/null +++ b/Crypto/Signing.hh @@ -0,0 +1,189 @@ +// +// Signing.hh +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#pragma once +#include "Base.hh" +#include "PublicKey.hh" + +namespace litecore::crypto { + + // Why create these interfaces, when PublicKey and PrivateKey already exist? + // The implementations of PublicKey and PrivateKey (and some of their API) are tied to mbedTLS. + // But mbedTLS doesn't implement Ed25519. So in order to create Ed25519PublicKey and + // Ed25519PrivateKey subclasses we'd have to heavily refactor the current `Key` class hierarchy + // and move the mbed-specific stuff to new `RSAPublicKey` and `RSAPrivateKey` classes. + // That didn't seem worth the trouble. + + static constexpr const char* kRSAAlgorithmName = "RSA"; + static constexpr const char* kEd25519AlgorithmName = "Ed25519"; + + class VerifyingKey; + + + /** Abstract interface of (private) cryptographic keys that can create signatures. */ + class SigningKey { + public: + /// Static factory method that generates a random key-pair of the given algorithm. + /// Throws `error::CryptoError` if the algorithm is unknown or the data is invalid for it. + static std::unique_ptr generate(const char *algorithm); + + /// Generates a digital signature of `inputData` with this key. + virtual alloc_slice sign(slice inputData) const =0; + + /// Instantiates a matching public verifying key. + virtual std::unique_ptr verifyingKey() const =0; + + /// Returns the data of the corresponding (public) key that can be used to verify signatures. + virtual alloc_slice verifyingKeyData() const =0; + + /// The short name of the algorithm used, "RSA" or "Ed25519". + virtual const char* algorithmName() const =0; + + virtual ~SigningKey() = default; + }; + + + /** Abstract interface of (public) cryptographic keys that can verify signatures. */ + class VerifyingKey { + public: + /// Static factory method that creates a VerifyingKey, using the given algorithm, from the + /// given data. + /// Throws `error::CryptoError` if the algorithm is unknown or the data is invalid for it. + static std::unique_ptr instantiate(slice data, const char *algorithm); + + /// The key data in binary form. + virtual alloc_slice data() const =0; + + /// Returns true if `signature` is a valid signature, generated by my corresponding + /// `SigningKey`, of `data`. + virtual bool verifySignature(slice inputData, slice signature) const =0; + + /// The short name of the algorithm used, "RSA" or "Ed25519". (See constants above.) + virtual const char* algorithmName() const =0; + + virtual ~VerifyingKey() = default; + }; + + +#pragma mark - RSA: + + + /** Keys for the RSA signature algorithm with SHA256 digests. */ + + + /** Wrapper for an (RSA) PrivateKey object, that implements `SigningKey`. */ + class RSASigningKey : public SigningKey { + public: + RSASigningKey(PrivateKey *rsaKey) :_key(rsaKey) { } + RSASigningKey(Retained &&rsaKey) :_key(std::move(rsaKey)) { } + + const char* algorithmName() const override {return kRSAAlgorithmName;} + alloc_slice sign(slice data) const override; + std::unique_ptr verifyingKey() const override; + alloc_slice verifyingKeyData() const override {return _key->publicKeyData();} + + private: + Retained _key; + }; + + + /** Wrapper for an (RSA) PublicKey object, that implements `VerifyingKey`. */ + class RSAVerifyingKey : public VerifyingKey { + public: + RSAVerifyingKey(PublicKey *rsaKey) :_key(rsaKey) { } + RSAVerifyingKey(Retained &&rsaKey) :_key(std::move(rsaKey)) { } + explicit RSAVerifyingKey(slice data) :_key(new PublicKey(data)) { } + + const char* algorithmName() const override {return kRSAAlgorithmName;} + alloc_slice data() const override {return _key->publicKeyData(KeyFormat::DER);} + bool verifySignature(slice dat, + slice sig) const override; + private: + Retained _key; + }; + + +#pragma mark - Ed25519: + + /** Keys for the Ed25519 signature algorithm with SHA512 digests. + + Ed25519 is an elliptic curve algorithm, very similar to Curve25519, but used for signatures + instead of encryption. Its advantages over RSA are order-of-magnitude smaller key and + signature sizes (256 bits vs. 2048+) with equivalent security, and higher performance. + */ + + + class Ed25519VerifyingKey; + + // A base class, not used directly + class Ed25519Base { + public: + /// Size of a key, in bytes. + static constexpr size_t kKeySize = 32; + + /// Size of a signature, in bytes. + static constexpr size_t kSignatureSize = 64; + + protected: + Ed25519Base() = default; + explicit Ed25519Base(slice bytes); + slice _data() const {return {_bytes.data(), _bytes.size()};} + + std::array _bytes; + }; + + + /** A private key for the Ed25519 elliptic-curve signature algorithm. */ + class Ed25519SigningKey : public SigningKey, Ed25519Base { + public: + /// Generates a key-pair at random. + Ed25519SigningKey(); + + /// Generates a key-pair at random and returns the private signing key. + static Ed25519SigningKey generate() {return Ed25519SigningKey();} + + /// Constructs a key from its 32-byte data representation. + explicit Ed25519SigningKey(slice bytes) :Ed25519Base(bytes) { } + + /// Returns the key's data. This should only be stored in a secure location. + slice data() const {return Ed25519Base::_data();} + + /// Returns the corresponding public key. + /// @note This is somewhat expensive to compute, so consider reusing the object. + Ed25519VerifyingKey publicKey() const; + + const char* algorithmName() const override {return kEd25519AlgorithmName;} + alloc_slice sign(slice data) const override; + std::unique_ptr verifyingKey() const override; + alloc_slice verifyingKeyData() const override; + + private: + }; + + + /** A public key for the Ed25519 elliptic-curve signature algorithm with SHA512 digests. + Ed25519 is very similar to Curve25519, but used for signatures instead of encryption. + SHA512 is used to digest the input data; the elliptic curve signs the digest. */ + class Ed25519VerifyingKey : public VerifyingKey, Ed25519Base { + public: + /// Constructs a public verifying key from its 32-byte data representation. + explicit Ed25519VerifyingKey(slice bytes); + + const char* algorithmName() const override {return kEd25519AlgorithmName;} + alloc_slice data() const override {return alloc_slice(Ed25519Base::_data());} + bool verifySignature(slice data, slice signature) const override; + private: + friend class Ed25519SigningKey; + Ed25519VerifyingKey() = default; + }; + +} diff --git a/LiteCore/Support/Error.hh b/LiteCore/Support/Error.hh index 58b2362da..0c20f8985 100644 --- a/LiteCore/Support/Error.hh +++ b/LiteCore/Support/Error.hh @@ -144,6 +144,10 @@ namespace litecore { return a.domain == error::LiteCore && a.code == code; } + static inline bool operator!= (const error &a, const error &b) noexcept {return !(a == b);} + static inline bool operator!= (const error &a, error::LiteCoreError code) noexcept + {return !(a == code);} + // Like C assert() but throws an exception instead of aborting #ifdef __FILE_NAME__ diff --git a/LiteCore/tests/CMakeLists.txt b/LiteCore/tests/CMakeLists.txt index ea8853827..0775dafae 100644 --- a/LiteCore/tests/CMakeLists.txt +++ b/LiteCore/tests/CMakeLists.txt @@ -90,6 +90,7 @@ add_executable( ${TOP}Replicator/tests/CookieStoreTest.cc ${TOP}REST/Response.cc ${TOP}Crypto/CertificateTest.cc + ${TOP}Crypto/SignatureTest.cc ${TOP}LiteCore/Support/TestsCommon.cc main.cpp ) diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 97c8ab8e5..5c934b904 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -160,6 +160,10 @@ 27469D08233D719800A1EE1A /* PublicKey+Apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2762A02022EF8C4E00F9AB18 /* PublicKey+Apple.mm */; }; 27469D09233D719800A1EE1A /* mbedUtils.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2762A01C22EB933100F9AB18 /* mbedUtils.cc */; }; 2746C8E62639E88700A3B2CC /* ThreadUtil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2746C8E52639E88700A3B2CC /* ThreadUtil.cc */; }; + 2747A0A327963F0B00F286AF /* SignedDict.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2747A09E27963F0A00F286AF /* SignedDict.cc */; }; + 2747A1BE2798847300F286AF /* SignatureTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2747A1BD2798847300F286AF /* SignatureTest.cc */; }; + 2747A1C12798920600F286AF /* Signing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2747A1C02798920600F286AF /* Signing.cc */; }; + 2747A1C22798920600F286AF /* Signing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2747A1C02798920600F286AF /* Signing.cc */; }; 27480E37253A5D9C0091CF37 /* VectorRecordTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27480E36253A5D9C0091CF37 /* VectorRecordTest.cc */; }; 2749B9871EB298360068DBF9 /* RESTListener+Handlers.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2749B9861EB298360068DBF9 /* RESTListener+Handlers.cc */; }; 274B36D225B271F7001FC28D /* Version.cc in Sources */ = {isa = PBXBuildFile; fileRef = 274B36D125B271F7001FC28D /* Version.cc */; }; @@ -219,6 +223,8 @@ 27727C55230F279D0082BCC9 /* HTTPLogic.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27727C53230F279D0082BCC9 /* HTTPLogic.cc */; }; 27766E161982DA8E00CAA464 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; 2776AA272087FF6B004ACE85 /* LegacyAttachments.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2776AA252087FF6B004ACE85 /* LegacyAttachments.cc */; }; + 277C5C3327A8864B001BE212 /* monocypher.c in Sources */ = {isa = PBXBuildFile; fileRef = 277C5C3227A8864B001BE212 /* monocypher.c */; settings = {COMPILER_FLAGS = "-Wno-shorten-64-to-32 -Wno-shadow"; }; }; + 277C5C3627A88658001BE212 /* monocypher-ed25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 277C5C3427A88658001BE212 /* monocypher-ed25519.c */; }; 2783DF991D27436700F84E6E /* c4ThreadingTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2783DF981D27436700F84E6E /* c4ThreadingTest.cc */; }; 2787EB271F4C91B000DB97B0 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; 2787EB291F4C929C00DB97B0 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; @@ -1047,6 +1053,12 @@ 2747664520190841007B39D1 /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = ""; }; 2747664720190841007B39D1 /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sqlite3.c; sourceTree = ""; }; 2747664820190841007B39D1 /* sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3.h; sourceTree = ""; }; + 2747A09E27963F0A00F286AF /* SignedDict.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SignedDict.cc; sourceTree = ""; }; + 2747A0A227963F0B00F286AF /* SignedDict.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SignedDict.hh; sourceTree = ""; }; + 2747A1BD2798847300F286AF /* SignatureTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SignatureTest.cc; sourceTree = ""; }; + 2747A1BF2798920600F286AF /* Signing.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Signing.hh; sourceTree = ""; }; + 2747A1C02798920600F286AF /* Signing.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Signing.cc; sourceTree = ""; }; + 2747A1C52798ED3D00F286AF /* Signed Objects.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Signed Objects.md"; sourceTree = ""; }; 27480E36253A5D9C0091CF37 /* VectorRecordTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = VectorRecordTest.cc; sourceTree = ""; }; 27491C9A1E7B1001001DC54B /* c4Socket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = c4Socket.h; sourceTree = ""; }; 27491C9E1E7B2532001DC54B /* c4Socket.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4Socket.cc; sourceTree = ""; }; @@ -1197,6 +1209,10 @@ 2776AA262087FF6B004ACE85 /* LegacyAttachments.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LegacyAttachments.hh; sourceTree = ""; }; 2777146C1C5D6BDB003C0287 /* static_lib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = static_lib.xcconfig; sourceTree = ""; }; 2779CC6E1E85E4FC00F0D251 /* ReplicatorTypes.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ReplicatorTypes.hh; sourceTree = ""; }; + 277C5C3127A8864B001BE212 /* monocypher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = monocypher.h; path = src/monocypher.h; sourceTree = ""; }; + 277C5C3227A8864B001BE212 /* monocypher.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = monocypher.c; path = src/monocypher.c; sourceTree = ""; }; + 277C5C3427A88658001BE212 /* monocypher-ed25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "monocypher-ed25519.c"; path = "src/optional/monocypher-ed25519.c"; sourceTree = ""; }; + 277C5C3527A88658001BE212 /* monocypher-ed25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "monocypher-ed25519.h"; path = "src/optional/monocypher-ed25519.h"; sourceTree = ""; }; 277CB6251D0DED5E00702E56 /* Fleece.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Fleece.xcodeproj; path = fleece/Fleece.xcodeproj; sourceTree = ""; }; 277D19C9194E295B008E91EB /* Error.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Error.hh; sourceTree = ""; }; 277FEE5721ED10FA00B60E3C /* ReplicatorSGTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ReplicatorSGTest.cc; sourceTree = ""; }; @@ -1227,7 +1243,6 @@ 279DE3DD24788A030059AE4E /* c4_ee.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = c4_ee.txt; path = scripts/c4_ee.txt; sourceTree = ""; }; 27A16314201FC2A500C18D9C /* DataFile+Shared.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = "DataFile+Shared.hh"; sourceTree = ""; }; 27A657BE1CBC1A3D00A7A1D7 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; - 27A83D4F269E35BC002B7EBA /* PropertyEncryption.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = PropertyEncryption.cc; path = "../../../../CBL/couchbase-lite-core-EE/Replicator/PropertyEncryption.cc"; sourceTree = ""; }; 27A83D53269E3E69002B7EBA /* PropertyEncryptionTests.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PropertyEncryptionTests.cc; sourceTree = ""; }; 27A83D57269F7DB2002B7EBA /* PropertyEncryption_stub.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PropertyEncryption_stub.cc; sourceTree = ""; }; 27A83D5C269F7F0E002B7EBA /* PropertyEncryption.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PropertyEncryption.hh; sourceTree = ""; }; @@ -2116,6 +2131,7 @@ 2745B7D626825F970012A17A /* Class Diagram.png */, 2745B7D026825F970012A17A /* Replicator Diagram.svg */, 2745B7CD26825F970012A17A /* Replicator Diagram.t2d */, + 2747A1C52798ED3D00F286AF /* Signed Objects.md */, ); path = overview; sourceTree = ""; @@ -2306,6 +2322,10 @@ 2762A01822EB92B900F9AB18 /* PublicKey.cc */, 2762A02022EF8C4E00F9AB18 /* PublicKey+Apple.mm */, 2743E31425F81406006F696D /* PublicKey+Windows.cc */, + 2747A1BF2798920600F286AF /* Signing.hh */, + 2747A1C02798920600F286AF /* Signing.cc */, + 2747A0A227963F0B00F286AF /* SignedDict.hh */, + 2747A09E27963F0A00F286AF /* SignedDict.cc */, 2762A01B22EB933100F9AB18 /* mbedUtils.hh */, 2762A01C22EB933100F9AB18 /* mbedUtils.cc */, 2716F91D248578D000BE21D9 /* mbedSnippets.hh */, @@ -2322,6 +2342,7 @@ isa = PBXGroup; children = ( 2762A01422EB7CC800F9AB18 /* CertificateTest.cc */, + 2747A1BD2798847300F286AF /* SignatureTest.cc */, ); name = tests; sourceTree = ""; @@ -2633,6 +2654,17 @@ name = Support; sourceTree = ""; }; + 277C5C3027A8861F001BE212 /* Monocypher */ = { + isa = PBXGroup; + children = ( + 277C5C3227A8864B001BE212 /* monocypher.c */, + 277C5C3127A8864B001BE212 /* monocypher.h */, + 277C5C3427A88658001BE212 /* monocypher-ed25519.c */, + 277C5C3527A88658001BE212 /* monocypher-ed25519.h */, + ); + path = Monocypher; + sourceTree = ""; + }; 27A924951D9B316D00086206 /* LiteCore iOS */ = { isa = PBXGroup; children = ( @@ -2845,6 +2877,7 @@ 277CB6251D0DED5E00702E56 /* Fleece.xcodeproj */, 275E6B9C22C2A3860032362A /* ios-cmake */, 2771A01F2284EA9F00B18E0A /* mbedtls */, + 277C5C3027A8861F001BE212 /* Monocypher */, 27AFF38F23036A7100B4D6C4 /* socketpp */, 27EF7FFA1914296D00A327B9 /* sqlite3-unicodesn */, 27D74A731D4D3F0700D806E0 /* SQLiteCpp */, @@ -3943,9 +3976,11 @@ 2761F3F71EEA00C3006D4BB8 /* CookieStoreTest.cc in Sources */, 2762A01522EB7CC800F9AB18 /* CertificateTest.cc in Sources */, 272850EA1E9D4860009CA22F /* ReplicatorLoopbackTest.cc in Sources */, + 2747A1C22798920600F286AF /* Signing.cc in Sources */, 27E19D662316EDEA00E031F8 /* RESTClientTest.cc in Sources */, 27B9669723284F2900B2897F /* RESTListenerTest.cc in Sources */, 27AFF3BA2303758E00B4D6C4 /* ReplicatorAPITest.cc in Sources */, + 2747A1BE2798847300F286AF /* SignatureTest.cc in Sources */, 277071D5230B682100F7EB95 /* SyncListenerTest.cc in Sources */, 27AFF3BB2303759400B4D6C4 /* ReplicatorSGTest.cc in Sources */, 27480E37253A5D9C0091CF37 /* VectorRecordTest.cc in Sources */, @@ -4217,6 +4252,7 @@ 27469D08233D719800A1EE1A /* PublicKey+Apple.mm in Sources */, 274B36D225B271F7001FC28D /* Version.cc in Sources */, 2744B351241854F2005A194D /* WebSocketImpl.cc in Sources */, + 2747A0A327963F0B00F286AF /* SignedDict.cc in Sources */, 2769438C1DCD502A00DB2555 /* c4Observer.cc in Sources */, 2744B354241854F2005A194D /* Actor.cc in Sources */, 2705154D1D8CBE6C00D62D05 /* c4Query.cc in Sources */, @@ -4224,6 +4260,7 @@ 275E4CCC22417D13006C5B71 /* Inserter.cc in Sources */, 2746C8E62639E88700A3B2CC /* ThreadUtil.cc in Sources */, 2744B34F241854F2005A194D /* Headers.cc in Sources */, + 2747A1C12798920600F286AF /* Signing.cc in Sources */, 2722504E1D7892610006D5A5 /* c4BlobStore.cc in Sources */, 275E9905238360B200EA516B /* Checkpointer.cc in Sources */, 275B35A5234E753800FE9CF0 /* Housekeeper.cc in Sources */, @@ -4281,6 +4318,7 @@ 27E487231922A64F007D8940 /* RevTree.cc in Sources */, 27E89BA61D679542002C32B3 /* FilePath.cc in Sources */, 279C18F01DF2051600D3221D /* SQLiteFTSRankFunction.cc in Sources */, + 277C5C3327A8864B001BE212 /* monocypher.c in Sources */, 27E6DFF01DA5AFF3008EB681 /* Query.cc in Sources */, 27D74A7E1D4D3F2300D806E0 /* Database.cpp in Sources */, 27ADA79B1F2BF64100D9DE25 /* UnicodeCollator.cc in Sources */, @@ -4290,6 +4328,7 @@ 274EDDF61DA30B43003AD158 /* QueryParser.cc in Sources */, 273E9F741C51612E003115A6 /* c4DocEnumerator.cc in Sources */, 270C6B8C1EBA2CD600E73415 /* LogEncoder.cc in Sources */, + 277C5C3627A88658001BE212 /* monocypher-ed25519.c in Sources */, 27D9655F2335667A00F4A51C /* SecureRandomize.cc in Sources */, 273D25F62564666A008643D2 /* VectorDocument.cc in Sources */, 27CCD4AF2315DB11003DEB99 /* Address.cc in Sources */, diff --git a/cmake/platform_base.cmake b/cmake/platform_base.cmake index 55724b5c9..4f0e6f580 100644 --- a/cmake/platform_base.cmake +++ b/cmake/platform_base.cmake @@ -33,6 +33,8 @@ function(set_litecore_source_base) Crypto/PublicKey.cc Crypto/SecureDigest.cc Crypto/SecureSymmetricCrypto.cc + Crypto/Signing.cc + Crypto/SignedDict.cc LiteCore/BlobStore/BlobStreams.cc LiteCore/BlobStore/Stream.cc LiteCore/Database/BackgroundDB.cc @@ -82,6 +84,8 @@ function(set_litecore_source_base) LiteCore/Storage/UnicodeCollator.cc Networking/Address.cc Networking/HTTP/CookieStore.cc + vendor/Monocypher/src/monocypher.c + vendor/Monocypher/src/optional/monocypher-ed25519.c vendor/SQLiteCpp/src/Backup.cpp vendor/SQLiteCpp/src/Column.cpp vendor/SQLiteCpp/src/Database.cpp diff --git a/docs/overview/Signed Objects.md b/docs/overview/Signed Objects.md new file mode 100644 index 000000000..7ae883d90 --- /dev/null +++ b/docs/overview/Signed Objects.md @@ -0,0 +1,179 @@ +# Signed JSON Objects and Documents + +January 19, 2022 + +This is a specification for digitally signing a JSON object, intended for use with document databases like Couchbase. + +## 1. Introduction + +Signing a document provides these benefits: +* The enclosed public key can be used as an identifier of the entity that signed the document. +* Any unauthorized modification of the document can be detected, as it'll invalidate the signature. + +Thus a signature serves as a form of authentication. Why do we need this when servers like Sync Gateway already support several types of authentication? +* The signature authenticates a _document_, not a _connection_. This is a very important distinction when documents are replicated, especially when they can pass between multiple servers. A document may be forwarded by an entity that didn't create it, so the fact that the replicator connection is authenticated does _not_ authenticate the document. The document has to carry its own credentials. +* Public keys allow for many types of identity and authentication. In the simplest case, an entity can create a key-pair and use the public key as its sole identification; this is useful even though it doesn't tie that entity to any external form of ID. More complex systems can use a hierarchical public-key infrastructure like X.509 or a "web of trust" like PGP. + +**History:** + +* 2009: Initial draft suggested to the CouchDB community. Heavily influenced by [SDSI](http://groups.csail.mit.edu/cis/sdsi.html), an experimental public-key infrastructure that used S-expressions as its universal data representation. +* Feb/March 2014: Significant evolution/rewrite of the old spec. First appearance in the Couchbase Lite wiki. +* August 2014: Simplified the data format by removing the use of nested arrays tagged with algorithm IDs. Added support for Curve25519 algorithm. +* July 2015: Replaced references to Curve25519 with Ed25519, since the latter is what's actually used for signatures (Curve25519 itself only does encryption.) +* January 2022: Revised for Couchbase Lite Core. Changed various property names. Changed canonical-JSON description to match the reality of what Fleece’s JSON encoder does. + +## 2. Usage + +There are two main algorithms here: the **signature algorithm** takes a JSON object and a private key and produces a signature object, and the **verification algorithm** takes a JSON object and a signature object and determines whether or not the signature is valid for that object. + +> **Note:** These algorithms will work with objects in any JSON-equivalent data format or API, such as Couchbase’s Fleece. The only time they require actual literal JSON is in the production of a canonical encoding, but that canonical JSON never appears anywhere, it just disappears into a SHA digest. + +Unlike some other JSON-signature systems, the object being signed doesn't need to be specially encoded. This is important because it doesn't get in the way of applications or middleware that read and write the objects. + +Another advantage is that the signature doesn't need to be contained in the signed object (or vice versa.) It is common for the signature to be contained -- and there's a special `(sig)` property defined for it -- but there are situations where this isn't practical. In this case it's up to the application to have a way to find the signature of an object. + +## 3. Data Formats + +### Cryptographic Algorithms and Formats + +This spec uses the SHA family of digest algorithms, and RSA or [Ed25519](http://ed25519.cr.yp.to/) signatures. Where digests and signatures appear in object properties, they are base64-encoded and their types are identified by adding a suffix to the property name. The suffixes are `SHA`, `RSA`, and `Ed25519`. + +For example, a SHA-256 digest looks like: + +```json +"digest_SHA": "0yiour/fLeTxyK2O5nOjRt8PwYbX/R/oq27/y5vtfcA=" +``` + +Notes: + +* SHA and RSA have multiple key or output sizes; the size doesn't need to be given explicitly in the property name because it can easily be inferred from the length of the associated binary data. + +* RSA signatures are produced from a SHA-256 digest of the input data. They are the same size as the key length (e.g. 2048 bits = 256 bytes.) + +* RSA public keys have multiple binary representations. Use the BSAFE (ASN.1 DER) format. +* Ed25519 signatures are produced from a SHA-512 digest of the input data. They are 64 bytes long. + +* Ed25519 public keys are 32 bytes. + +Other algorithms could be added in the future; they just need standard suffix strings. + +### Signature Object + +This is a JSON object that acts as a digital signature of _some other JSON object_ (without specifying where that other object is.) + +Absolutely-required properties, each of which has a base64 string as its value: + +* `digest_SHA`: A cryptographic digest of the canonical JSON encoding of the object being signed. +* `sig_RSA` or `sig_Ed25519`: The digital signature of the canonical encoding of the signature object _minus this field_. + +Usually-required properties that will be present in most use cases: + +* `key`: The public key of the key-pair performing the signing. (The algorithm doesn't need to be specified: it's the same as the one named by the `sig_xx` property.) The key could be omitted if the the verifying party will know the signer’s public key through some other means. For example, the document might include an identifier that can be used to look it up. +* `date`: A timestamp identifying when the signature was generated. May be an ISO-8601-format string (example: `"2014-08-29T16:22:28Z"`), or an integer denoting the number of milliseconds since the Unix epoch Jan 1 1970 00:00AM UTC. + +* `expires`: The number of **minutes** the signature remains valid after being generated. + +### Signed Object + +This is simply a JSON object that directly contains its signature as the value of a `(sig)` property. Obviously this property needs to be ignored while computing the canonical digest of the object. + +## 4. Algorithms + +### Canonical Digests of JSON (compatible) Objects + +Digest algorithms like SHA operate on raw binary data, not abstract objects like JSON or Fleece. Unfortunately a JSON value can be encoded in many slightly different ways; you can change the order of object keys, escape different characters in strings, add or omit whitespace. All of those differences will result in different digests. + +So for the signer and verifier to agree on the same digest of an object, there has to be a canonical encoding algorithm that always maps equivalent objects to identical data. + +Therefore the algorithm to create a digest of JSON is: + +1. (Re-)encode the object in canonical JSON encoding (q.v.). +2. Compute a digest of the UTF-8 byte-string produced. + +### Canonical JSON Encoding + +There have been various rules proposed for canonicalizing JSON, and of course they don’t all agree. The differences are mostly in the handling of strings and object keys. Here are the rules we use: + +* No whitespace. +* Numbers should be integers in the range [-2^47^ .. 2^47^-1]. +* Numbers cannot be encoded with decimal points, scientific notation, or leading zeros. "`-0`" is not allowed either. (`NaN` and `Inf` are right out — they’re not valid JSON at all.) +* Strings (including keys) are converted to [Unicode Normalization Form C](http://www.unicode.org/reports/tr15/). +* Strings use the escape sequences `\\`, `\"`, `\r`, `\n`, `\t`. Other control characters (00-1F and 7F) are escaped using `\uxxxx`. All other characters are written literally as UTF-8, including non-ASCII ones. +* Object keys are lexicographically sorted by their UTF-8 string representation, i.e. as by `strcmp`. +* The output text encoding is of course UTF-8. + +Non-integers are strongly discouraged because different formatting libraries will convert them to decimal form in different ways (e.g. `1.1` vs `1.099999`). Even integers are limited to 48-bit, not 64-bit, because many JSON parsers convert all numbers to double-precision floating point, which is a 64-bit value but only has about 50 bits of precision (mantissa), and drops the least significant bits of integers outside ~ ±2^50^. + +> **Note:** The Fleece library’s JSON encoder produces this encoding when its `canonical` parameter is `true`. It does not reject invalid numbers, just encodes them as well as it can. Thus, if you sign an object containing invalid numbers, it’s pretty likely it will validate correctly in any other system using Fleece (i.e. Couchbase Lite). But other software implementing these algorithms may not encode the numbers exactly the same, which would break verification. + +### Creating A JSON Signature Object + +1. Compute the SHA-256 digest of the canonical JSON of the object being signed. +2. Create an empty object to hold the signature, and store a base64 string of the digest in its `digest_SHA` property. +2. Store a base64 string of the public key in the `key` property (unless the key will already be known to the verifier and you want to save room by omitting it from the signature.) +3. Store the current date/time in the `date` property, as an ISO-8601 string or a milliseconds-since-Unix-epoch integer. +3. Store the duration of validity, in minutes, as an integer in the `expires` property. +4. Add any other optional properties desired. (See *Usage With Document Databases*, below.) +5. Compute the canonical JSON of the signature object you just constructed. +6. Generate a digital signature of that canonical JSON, using the private key that matches the public key used in step 2. (This will involve computing a digest and then signing the digest; some crypto APIs make that explicit, and some do it for you.) +7. Add that signature, base64-encoded, as the `sig_RSA` or `sig_Ed25519` property of the signature object. + +### Verifying A JSON Signature Object + +1. Temporarily remove from the target object (the one the signature signed) any properties that weren’t present when it was signed; for instance, the `(sig)` property itself, if the object contains its own signature. +1. Compute the canonical digest of the target object, using the algorithm given in the signature object's `digest` property. +2. Compare this digest with the one contained in the `digest` property of the signature. If they aren't equal, **fail (the object does not match what was signed.)** +3. Copy the signature object and remove the `sig` property from the copy. +4. Compute the canonical digest of the copied signature object. +5. Verify the digest against the signature contained in the `sig` property, using the public key contained in the `key` property. If verification fails, **fail (the signature object itself has been altered.)** +7. If the signature contains a `date` property: + 1. Decode the date as an ISO-8601 or integer timestamp. If that date is more than one minute in the future, **fail (not valid yet, or else there's unacceptable clock skew.)** + 2. There must be an `expires` property, containing a positive integer. Add that number of minutes to the `date`. If the resulting time is in the past, **fail (signature expired).** + +8. **Succeed: the signature is valid!** + +At any step: +* If any value in the signature object is invalid (invalid ISO-8601, invalid base64, etc.), then **fail (the signature is syntactically invalid).** +* If any algorithm string is unrecognized or the program can't perform that algorithm, then **fail (not possible to verify the signature.)** It is not known whether the signature is valid, but the application should _not_ trust the signature or the object that was signed. + +## 5. Usage With Document Databases + +When signing documents belonging to a Couchbase database it's important to handle the document metadata correctly. + +The key point is that **the document ID and revision ID must be included in the signature**. If not, the document and signature can be used for replay attacks. If the doc ID isn't signed, an attacker can create a copy of the signed document under a different ID. If signature doesn't include the revision ID, an attacker can re-post the signed revision at any time, reverting the document to an older version. + +Note: To the verifier, the revision ID in the signature is actually the *parent* rev ID of the signed revision. It was current at the time the revision was being signed. (The new revision’s ID isn’t known until after it’s been saved, which is too late to change anything in it!) + + +Specifically: + +* The document ID must be added to the signature as a `docID` property. +* The current revisionID at the time the new revision is being signed must be added to the signature as a `parentRev` property, unless this is a first-generation document with no parent revision. + +## 6. Example + +A signature object using Ed25519, created January 18, 2022 at 12:52 PST with a five-minute expiration time: +```json +{ + "digest_SHA": "0yiour/fLeTxyK2O5nOjRt8PwYbX/R/oq27/y5vtfcA=", + "key": "RjhO2DQvPfa5A+YtpCYHxg0jajjfyLIAryANpe/MxCA=", + "sig_Ed25519": "pvr9sLAjEJx+D6DfE0kjwO+gbcI5WUgaZTiDvliddXfGRbALeo1tcppPmsGDujN3ZoEojVk7g1BykgVR3kM+AA==", + "date": 1642632165223, + "expires": 5 +} +``` + +The same signature, embedded in the document it signs: +```json +{ + "age": 6, + "name": "Oliver Bolliver Butz", + "(sig)": { + "digest_SHA": "0yiour/fLeTxyK2O5nOjRt8PwYbX/R/oq27/y5vtfcA=", + "key": "RjhO2DQvPfa5A+YtpCYHxg0jajjfyLIAryANpe/MxCA=", + "sig_Ed25519": "pvr9sLAjEJx+D6DfE0kjwO+gbcI5WUgaZTiDvliddXfGRbALeo1tcppPmsGDujN3ZoEojVk7g1BykgVR3kM+AA==", + "date": 1642632165223, + "expires": 5 + } +} +``` \ No newline at end of file diff --git a/vendor/Monocypher b/vendor/Monocypher new file mode 160000 index 000000000..baca5d312 --- /dev/null +++ b/vendor/Monocypher @@ -0,0 +1 @@ +Subproject commit baca5d31259c598540e4d1284bc8d8f793abf83a diff --git a/vendor/fleece b/vendor/fleece index f47bba026..08001367f 160000 --- a/vendor/fleece +++ b/vendor/fleece @@ -1 +1 @@ -Subproject commit f47bba026f062cb23072f152350d509598a5d867 +Subproject commit 08001367ff9191e5532752c82b73921fec7eca3a