Skip to content

Commit

Permalink
Signed-Dict support (RSA and Ed25519)
Browse files Browse the repository at this point in the history
- Abstract API for keys that create and verify signatures.
  (I couldn't just use PublicKey and PrivateKey because they are
  tightly tied to mbedTLS and RSA.)
- Implementation of it using PublicKey / PrivateKey.
- Implementation of it using Ed25519 keys.
- Added Monocypher submodule: a tiny crypto library that implements
  Ed25519.
- API for signed Fleece values, using the signed-dict data format
  that I came up with years ago.
- Unit test.
  • Loading branch information
snej committed Jan 19, 2022
1 parent 5e17f14 commit 08d3136
Show file tree
Hide file tree
Showing 13 changed files with 718 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
[submodule "vendor/sqlite3-unicodesn"]
path = vendor/sqlite3-unicodesn
url = https://github.com/couchbasedeps/sqlite3-unicodesn
[submodule "vendor/monocypher-cpp"]
path = vendor/monocypher-cpp
url = https://github.com/snej/monocypher-cpp.git
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ target_include_directories(
vendor/sqlite3-unicodesn
vendor/mbedtls/include
vendor/mbedtls/crypto/include
vendor/monocypher-cpp/include
vendor/monocypher-cpp/vendor/monocypher/src
vendor/monocypher-cpp/vendor/monocypher/src/optionsl
vendor/sockpp/include
)

Expand Down
60 changes: 60 additions & 0 deletions Crypto/SignatureTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
//

#include "PublicKey.hh"
#include "SignedDict.hh"
#include "Base64.hh"
#include "Error.hh"
#include "LiteCoreTest.hh"
#include "fleece/Mutable.hh"
#include <iostream>


Expand Down Expand Up @@ -46,3 +48,61 @@ TEST_CASE("RSA Signatures", "[Signatures]") {
((uint8_t&)signature[100])++;
CHECK(!key->publicKey()->verifySignature(kDataToSign, signature));
}


TEST_CASE("Signed Document", "[Signatures]") {
bool embedKey = GENERATE(false, true);
cout << "---- Embed key in signature = " << embedKey << endl;

// Create a signed doc and convert to JSON:
alloc_slice publicKeyData;
string json;
{
auto priv = Ed25519SigningKey::generate();
auto pub = priv.publicKey();
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) == 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, "Ed25519");
if (embedKey) {
REQUIRE(parsedKey);
CHECK(parsedKey->data() == publicKeyData);
} else {
CHECK(!parsedKey);
parsedKey = make_unique<Ed25519VerifyingKey>(publicKeyData);
}

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);
}
}
188 changes: 188 additions & 0 deletions Crypto/SignedDict.cc
Original file line number Diff line number Diff line change
@@ -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<VerifyingKey> 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<VerifyingKey> 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<VerifyingKey> 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;
}

}
83 changes: 83 additions & 0 deletions Crypto/SignedDict.hh
Original file line number Diff line number Diff line change
@@ -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<VerifyingKey> 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<VerifyingKey> 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);
}
Loading

0 comments on commit 08d3136

Please sign in to comment.