-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
13 changed files
with
718 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.