|
| 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 | +} |
0 commit comments