Skip to content

Commit 08d3136

Browse files
committed
Signed-Dict support (RSA and Ed25519)
- 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.
1 parent 5e17f14 commit 08d3136

File tree

13 files changed

+718
-1
lines changed

13 files changed

+718
-1
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@
2222
[submodule "vendor/sqlite3-unicodesn"]
2323
path = vendor/sqlite3-unicodesn
2424
url = https://github.com/couchbasedeps/sqlite3-unicodesn
25+
[submodule "vendor/monocypher-cpp"]
26+
path = vendor/monocypher-cpp
27+
url = https://github.com/snej/monocypher-cpp.git

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,9 @@ target_include_directories(
285285
vendor/sqlite3-unicodesn
286286
vendor/mbedtls/include
287287
vendor/mbedtls/crypto/include
288+
vendor/monocypher-cpp/include
289+
vendor/monocypher-cpp/vendor/monocypher/src
290+
vendor/monocypher-cpp/vendor/monocypher/src/optionsl
288291
vendor/sockpp/include
289292
)
290293

Crypto/SignatureTest.cc

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
//
1212

1313
#include "PublicKey.hh"
14+
#include "SignedDict.hh"
1415
#include "Base64.hh"
1516
#include "Error.hh"
1617
#include "LiteCoreTest.hh"
18+
#include "fleece/Mutable.hh"
1719
#include <iostream>
1820

1921

@@ -46,3 +48,61 @@ TEST_CASE("RSA Signatures", "[Signatures]") {
4648
((uint8_t&)signature[100])++;
4749
CHECK(!key->publicKey()->verifySignature(kDataToSign, signature));
4850
}
51+
52+
53+
TEST_CASE("Signed Document", "[Signatures]") {
54+
bool embedKey = GENERATE(false, true);
55+
cout << "---- Embed key in signature = " << embedKey << endl;
56+
57+
// Create a signed doc and convert to JSON:
58+
alloc_slice publicKeyData;
59+
string json;
60+
{
61+
auto priv = Ed25519SigningKey::generate();
62+
auto pub = priv.publicKey();
63+
publicKeyData = pub.data();
64+
65+
MutableDict doc = MutableDict::newDict();
66+
doc["name"] = "Oliver Bolliver Butz";
67+
doc["age"] = 6;
68+
cout << "Document: " << doc.toJSONString() << endl;
69+
70+
MutableDict sig = makeSignature(doc, priv, 5 /*minutes*/, embedKey);
71+
REQUIRE(sig);
72+
string sigJson = sig.toJSONString();
73+
cout << "Signature, " << sigJson.size() << " bytes: " << sigJson << endl;
74+
75+
CHECK(verifySignature(doc, sig, &pub) == VerifyResult::Valid);
76+
77+
doc["(sig)"] = sig; // <-- add signature to doc, in "(sig)" property
78+
json = doc.toJSONString();
79+
}
80+
cout << "Signed Document: " << json << endl;
81+
82+
// Now parse the JSON and verify the signature:
83+
{
84+
Doc parsedDoc = Doc::fromJSON(json);
85+
Dict doc = parsedDoc.asDict();
86+
Dict sig = doc["(sig)"].asDict();
87+
REQUIRE(sig);
88+
89+
auto parsedKey = getSignaturePublicKey(sig, "Ed25519");
90+
if (embedKey) {
91+
REQUIRE(parsedKey);
92+
CHECK(parsedKey->data() == publicKeyData);
93+
} else {
94+
CHECK(!parsedKey);
95+
parsedKey = make_unique<Ed25519VerifyingKey>(publicKeyData);
96+
}
97+
98+
MutableDict unsignedDoc = doc.mutableCopy();
99+
unsignedDoc.remove("(sig)"); // <-- detach signature to restore doc to signed form
100+
101+
if (embedKey)
102+
CHECK(verifySignature(unsignedDoc, sig) == VerifyResult::Valid);
103+
else
104+
CHECK(verifySignature(unsignedDoc, sig) == VerifyResult::MissingKey);
105+
106+
CHECK(verifySignature(unsignedDoc, sig, parsedKey.get()) == VerifyResult::Valid);
107+
}
108+
}

Crypto/SignedDict.cc

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//
2+
// SignedDict.cc
3+
//
4+
// Copyright 2022-Present Couchbase, Inc.
5+
//
6+
// Use of this software is governed by the Business Source License included
7+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
8+
// in that file, in accordance with the Business Source License, use of this
9+
// software will be governed by the Apache License, Version 2.0, included in
10+
// the file licenses/APL2.txt.
11+
//
12+
13+
#include "SignedDict.hh"
14+
#include "SecureDigest.hh"
15+
#include "Error.hh"
16+
#include "fleece/Mutable.hh"
17+
#include "Base64.hh"
18+
19+
namespace litecore::crypto {
20+
using namespace std::string_literals;
21+
using namespace fleece;
22+
23+
/*
24+
Signature dict schema:
25+
{
26+
"sig_RSA"
27+
or "sig_Ed25519": A digital signature of the canonical JSON form of this signature
28+
dict itself. (When verifying, this property must be removed
29+
since it didn't exist when the signature was being computed.)
30+
The suffix after "sig_" is the value of `SigningKey::algorithmName()`.
31+
"digest_SHA": A SHA digest of the canonical JSON of the value being signed.
32+
Usually SHA256; the specific algorithm can be determined by the data's size.
33+
"key": The [optional] public key data for verifying the signature.
34+
The algorithm is the same as indicated by the "sig_..." property's suffix.
35+
If not present, the verifier must know the key through some other means
36+
and pass it to `verifySignature()`.
37+
"date": A timestamp of when the signature was created.
38+
"expires": The number of minutes before the signature expires.
39+
}
40+
41+
Other optional application-defined properties may be added to the signature dict.
42+
They become part of the signature, so they cannot be tampered with,
43+
but the signature verification code here doesn't pay any attention to them.
44+
45+
- Data is either a base64-encoded string, or a Fleece data value.
46+
- A timestamp is either a number of milliseconds since the Unix epoch, or an ISO-8601 string.
47+
- Canonical JSON rules:
48+
* No whitespace.
49+
* Dicts are ordered by sorting the keys lexicographically (before encoding them as JSON.)
50+
* Strings use only the escape sequences `\\`, `\"`, `\r`, `\n`, `\t`, and the generic
51+
escape sequence `\uxxxx` for other control characters and 0x7F. All others are literal,
52+
including non-ASCII UTF-8 sequences.
53+
* No leading zeroes in integers, and no `-` in front of `0`.
54+
* Floating-point numbers should be avoided since there's no universally recognized algorithm
55+
to convert them to decimal, so different encoders may produce different results.
56+
*/
57+
58+
59+
// The amount by which a signature's start date may be in the future and still be considered
60+
// valid when verifying it.
61+
// This compensates for clock inconsistency between computers: if you create a signature and
62+
// immediately send it over the network to someone else, but their system clock is slightly
63+
// behind yours, they will probably see the signature's date as being in the future. Without
64+
// some allowance for this, they'd reject the signature.
65+
// In other words, this is the maximum clock variance we allow when verifying a just-created
66+
// signature.
67+
static constexpr int64_t kClockDriftAllowanceMS = 60 * 1000;
68+
69+
70+
MutableDict makeSignature(Value toBeSigned,
71+
const SigningKey &privateKey,
72+
int64_t expirationTimeMinutes,
73+
bool embedPublicKey,
74+
Dict otherMetadata)
75+
{
76+
// Create a signature object containing the document digest and the public key:
77+
MutableDict signature = otherMetadata ? otherMetadata.mutableCopy() : MutableDict::newDict();
78+
SHA256 digest(toBeSigned.toJSON(false, true));
79+
signature["digest_SHA"].setData(digest);
80+
if (embedPublicKey)
81+
signature["key"].setData(privateKey.verifyingKeyData());
82+
if (expirationTimeMinutes > 0) {
83+
if (!signature["date"])
84+
signature["date"] = FLTimestamp_Now();// alloc_slice(FLTimestamp_ToString(FLTimestamp_Now(), false));
85+
if (!signature["expires"])
86+
signature["expires"] = expirationTimeMinutes;
87+
}
88+
89+
// Sign the signature object, add the signature, and return it:
90+
alloc_slice signatureData = privateKey.sign(signature.toJSON(false, true));
91+
signature["sig_"s + privateKey.algorithmName()].setData(signatureData);
92+
return signature;
93+
}
94+
95+
96+
static alloc_slice convertToData(Value dataOrStr) {
97+
if (slice data = dataOrStr.asData(); data)
98+
return alloc_slice(data);
99+
else if (slice str = dataOrStr.asString(); str)
100+
return base64::decode(str);
101+
else
102+
return nullslice;
103+
}
104+
105+
106+
unique_ptr<VerifyingKey> getSignaturePublicKey(Dict signature, const char *algorithmName) {
107+
alloc_slice data = convertToData(signature["key"]);
108+
if (!data)
109+
return nullptr;
110+
if (!signature["sig_"s + algorithmName])
111+
return nullptr;
112+
return VerifyingKey::instantiate(data, algorithmName);
113+
}
114+
115+
116+
unique_ptr<VerifyingKey> getSignaturePublicKey(Dict signature) {
117+
auto key = getSignaturePublicKey(signature, kRSAAlgorithmName);
118+
if (!key)
119+
key = getSignaturePublicKey(signature, kEd25519AlgorithmName);
120+
return key;
121+
}
122+
123+
124+
VerifyResult verifySignature(Value toBeVerified,
125+
Dict signature,
126+
const VerifyingKey *publicKey)
127+
{
128+
// Get the digest property from the signature:
129+
Value digestVal = signature["digest_SHA"];
130+
if (!digestVal)
131+
return VerifyResult::InvalidProperties;
132+
auto digest = convertToData(digestVal);
133+
if (!digest || digest.size != sizeof(SHA256))
134+
return VerifyResult::InvalidProperties;
135+
136+
unique_ptr<VerifyingKey> embeddedKey;
137+
if (publicKey) {
138+
// If there's an embedded key, make sure it matches the key I was given:
139+
if (Value key = signature["key"]; key && convertToData(key) != publicKey->data())
140+
return VerifyResult::ConflictingKeys;
141+
} else {
142+
// If no public key was given, read it from the signature:
143+
embeddedKey = getSignaturePublicKey(signature);
144+
if (!embeddedKey)
145+
return VerifyResult::MissingKey;
146+
publicKey = embeddedKey.get();
147+
}
148+
149+
// Find the signature data itself:
150+
string sigProp = "sig_"s + publicKey->algorithmName();
151+
auto signatureData = convertToData(signature[sigProp]);
152+
if (!signatureData)
153+
return VerifyResult::InvalidProperties;
154+
155+
// Generate canonical JSON of the signature dict, minus the "sig_" property:
156+
MutableDict strippedSignature = signature.mutableCopy();
157+
strippedSignature.remove(sigProp);
158+
alloc_slice signedData = strippedSignature.toJSON(false, true);
159+
160+
// Verify the signature:
161+
if (!publicKey->verifySignature(signedData, signatureData))
162+
return VerifyResult::InvalidSignature;
163+
164+
// Verify that the digest matches that of the document:
165+
if (digest != SHA256(toBeVerified.toJSON(false, true)).asSlice())
166+
return VerifyResult::InvalidDigest;
167+
168+
// Verify that the signature is not expired nor not-yet-valid:
169+
if (Value date = signature["date"]; date) {
170+
FLTimestamp now = FLTimestamp_Now();
171+
FLTimestamp start = date.asTimestamp();
172+
if (start <= 0)
173+
return VerifyResult::InvalidProperties;
174+
if (now + kClockDriftAllowanceMS < start)
175+
return VerifyResult::Expired;
176+
if (Value exp = signature["expires"]; exp) {
177+
int64_t expMinutes = exp.asInt();
178+
if (expMinutes <= 0)
179+
return VerifyResult::InvalidProperties;
180+
if ((now - start) / 60000 > expMinutes)
181+
return VerifyResult::Expired;
182+
}
183+
}
184+
185+
return VerifyResult::Valid;
186+
}
187+
188+
}

Crypto/SignedDict.hh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// SignedDict.hh
3+
//
4+
// Copyright 2022-Present Couchbase, Inc.
5+
//
6+
// Use of this software is governed by the Business Source License included
7+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
8+
// in that file, in accordance with the Business Source License, use of this
9+
// software will be governed by the Apache License, Version 2.0, included in
10+
// the file licenses/APL2.txt.
11+
//
12+
13+
#pragma once
14+
#include "Base.hh"
15+
#include "Signing.hh"
16+
#include "fleece/Fleece.hh"
17+
18+
namespace litecore::crypto {
19+
20+
/// Possible results of verifying a signature.
21+
/// Any result other than `Valid` means the signature is not valid and the contents of the
22+
/// object are not to be trusted. The specific values might help in choosing an error message.
23+
enum class VerifyResult {
24+
Valid, ///< The signature is valid!
25+
Expired, ///< The signature was valid but has expired (or isn't valid yet.)
26+
MissingKey, ///< No key was given and there's no key embedded in the signature.
27+
ConflictingKeys, ///< Key given doesn't match public key embedded in signature.
28+
InvalidProperties, ///< Properties in the signature dict are missing or invalid.
29+
InvalidDigest, ///< Digest in signature doesn't match that of the signed object itself.
30+
InvalidSignature ///< The signature data itself didn't check out.
31+
};
32+
33+
34+
/// Creates a signature of a Fleece Value, usually a Dict.
35+
/// The signature takes the form of a Dict.
36+
/// @param toBeSigned The Fleece value, usually a Dict, to be signed.
37+
/// @param key A private key to sign with, RSA or Ed25519.
38+
/// @param expirationTimeMinutes How long until the signature expires. Units are **minutes**.
39+
/// Default value is one year.
40+
/// @param embedPublicKey If true, the public key data will be included in the signature object.
41+
/// If false it's omitted; then whoever verifies the signature
42+
/// must already know the public key through some other means.
43+
/// @param otherMetadata An optional Dict of other properties to add to the signature Dict.
44+
/// These properties will be signed, so any tampering of them will
45+
/// invalidate the signature just like tampering with `toBeSigned`.
46+
/// @return The signature object, a (mutable) Dict.
47+
[[nodiscard]]
48+
fleece::MutableDict makeSignature(fleece::Value toBeSigned,
49+
const SigningKey &key,
50+
int64_t expirationTimeMinutes = 60 * 24 * 365,
51+
bool embedPublicKey = true,
52+
fleece::Dict otherMetadata =nullptr);
53+
54+
55+
/// Returns the public key embedded in a signature, if there is one.
56+
/// Returns `nullptr` if the signature has no key data for any known algorithm.
57+
/// Throws `error::CryptoError` if the key data exists but is invalid.
58+
unique_ptr<VerifyingKey> getSignaturePublicKey(fleece::Dict signature);
59+
60+
61+
/// Returns the public key, with the given algorithm, embedded in a signature.
62+
/// Returns `nullptr` if the signature has no key data for that algorithm.
63+
/// Throws `error::CryptoError` if the key data exists but is invalid.
64+
unique_ptr<VerifyingKey> getSignaturePublicKey(fleece::Dict signature,
65+
const char *algorithmName);
66+
67+
68+
/// Verifies a signature of `document` using the signature object `signature`.
69+
/// The `document` must be _exactly the same_ as when it was signed; any properties added to it
70+
/// afterwards need to be removed. This probably includes the `signature` itself!
71+
/// @param toBeVerified The Fleece value which is to be verified.
72+
/// @param signature The signature. (Must not be contained in `toBeVerified`!)
73+
/// @param publicKey The `VerifyingKey` matching the `SigningKey` that made the signature.
74+
/// If `nullptr`, a key embedded in the signature will be used.
75+
/// @return An status value, which will be `Valid` if the signature is valid;
76+
/// or `MissingDigest` or `MissingKey` if no digest or key properties corresponding to
77+
/// the verifier were found;
78+
/// or other values if the signature itself is invalid or expired.
79+
[[nodiscard]]
80+
VerifyResult verifySignature(fleece::Value toBeVerified,
81+
fleece::Dict signature,
82+
const VerifyingKey *publicKey =nullptr);
83+
}

0 commit comments

Comments
 (0)