Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

api_safe_actions.inc

v0.1.0-build.85 Latest Commit ede38f5

Size
12 KB
Lines
249
Language
cpp
SHA-256
336d8956fdc9…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Public C ABI: Safe Actions (spec/CKey-Safe-Actions.md). Included after core_internal.inc.
2//
3// A Safe Action payload is the text "ckey-action:1:" followed by the base64url
4// (no padding) of a signed envelope:
5//
6// version u8 (1) | scope u8 (0 same-device, 1 cross-device)
7// issuer u8 len + bytes DNS name of the issuing site, lower case
8// app u8 len + bytes ID of the receiving app, e.g. com.viadere
9// action u8 len + bytes an action the receiving app registered
10// key_id u8 len + bytes which of the issuer's keys signed it
11// request u8 len + bytes single-use request ID, 8..64 characters
12// issued_at u64 BE, expires_at u64 BE Unix seconds
13// params u16 BE len + bytes at most 1024; UTF-8 JSON is recommended
14// signature 64 bytes Ed25519 over "CKey Safe Action v1\0" + every byte above
15//
16// The core parses, signs and verifies. Whether an action may run is decided by
17// the SDKs from the verified result and what the receiving app registered.
18
19#include "ed25519.inc"
20
21namespace {
22
23constexpr char kSafeActionPrefix[] = "ckey-action:1:";
24constexpr size_t kSafeActionPrefixLen = sizeof(kSafeActionPrefix) - 1;
25constexpr char kSafeActionContext[] = "CKey Safe Action v1"; // signed with its terminating NUL
26
27bool sa_issuer_ok(const uint8_t *p, size_t n) {
28 if (n < 1 || n > 253 || p[0] == '.' || p[n - 1] == '.') return false;
29 bool dot = false;
30 for (size_t i = 0; i < n; ++i) {
31 const uint8_t c = p[i];
32 if (c == '.') { if (i && p[i - 1] == '.') return false; dot = true; continue; }
33 if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) return false;
34 }
35 return dot;
36}
37
38bool sa_token_ok(const uint8_t *p, size_t n, size_t min, size_t max, bool lower_only, bool allow_dot) {
39 if (n < min || n > max) return false;
40 for (size_t i = 0; i < n; ++i) {
41 const uint8_t c = p[i];
42 const bool ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' ||
43 (!lower_only && c >= 'A' && c <= 'Z') || (allow_dot && c == '.');
44 if (!ok) return false;
45 }
46 return true;
47}
48
49int b64url_value(uint8_t c) {
50 if (c >= 'A' && c <= 'Z') return c - 'A';
51 if (c >= 'a' && c <= 'z') return c - 'a' + 26;
52 if (c >= '0' && c <= '9') return c - '0' + 52;
53 if (c == '-') return 62;
54 if (c == '_') return 63;
55 return -1;
56}
57
58bool b64url_decode(const uint8_t *in, size_t n, std::vector<uint8_t> &out) {
59 if (n % 4 == 1) return false;
60 out.clear();
61 out.reserve(n * 3 / 4);
62 uint32_t acc = 0;
63 int bits = 0;
64 for (size_t i = 0; i < n; ++i) {
65 const int v = b64url_value(in[i]);
66 if (v < 0) return false;
67 acc = (acc << 6) | static_cast<uint32_t>(v);
68 bits += 6;
69 if (bits >= 8) {
70 bits -= 8;
71 out.push_back(static_cast<uint8_t>(acc >> bits));
72 }
73 }
74 // Unused low bits must be zero, so each envelope has exactly one text form.
75 return (acc & ((1u << bits) - 1)) == 0;
76}
77
78void b64url_encode(const std::vector<uint8_t> &in, std::string &out) {
79 static const char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
80 uint32_t acc = 0;
81 int bits = 0;
82 for (uint8_t b : in) {
83 acc = (acc << 8) | b;
84 bits += 8;
85 while (bits >= 6) { bits -= 6; out.push_back(kAlphabet[(acc >> bits) & 63]); }
86 }
87 if (bits) out.push_back(kAlphabet[(acc << (6 - bits)) & 63]);
88}
89
90// Parses the decoded envelope into out; returns the offset of the signature, or 0 on error.
91size_t sa_parse_bytes(const std::vector<uint8_t> &b, ckey_safe_action *out, const char **why) {
92 size_t at = 0;
93 auto need = [&](size_t n) { return at + n <= b.size(); };
94 if (!need(2) || b[0] != 1) { *why = "unsupported Safe Action version"; return 0; }
95 if (b[1] > 1) { *why = "unknown scope"; return 0; }
96 std::memset(out, 0, sizeof(*out));
97 out->abi_version = CKEY_ABI_VERSION;
98 out->scope = b[1];
99 at = 2;
100 struct Field { char *dest; size_t cap; const char *name; int kind; size_t min, max; };
101 const Field fields[] = {
102 {out->issuer, sizeof(out->issuer), "issuer", 0, 1, 253},
103 {out->app, sizeof(out->app), "app", 1, 1, 255},
104 {out->action, sizeof(out->action), "action", 2, 1, 64},
105 {out->key_id, sizeof(out->key_id), "key ID", 3, 1, 32},
106 {out->request_id, sizeof(out->request_id), "request ID", 4, 8, 64},
107 };
108 for (const Field &f : fields) {
109 if (!need(1)) { *why = "truncated envelope"; return 0; }
110 const size_t n = b[at++];
111 if (!need(n)) { *why = "truncated envelope"; return 0; }
112 const uint8_t *p = b.data() + at;
113 const bool ok = f.kind == 0 ? sa_issuer_ok(p, n)
114 : f.kind == 1 ? sa_token_ok(p, n, f.min, f.max, false, true)
115 : f.kind == 2 ? sa_token_ok(p, n, f.min, f.max, true, true)
116 : f.kind == 3 ? sa_token_ok(p, n, f.min, f.max, false, true)
117 : sa_token_ok(p, n, f.min, f.max, false, false);
118 if (!ok || n + 1 > f.cap) { *why = f.name; return 0; }
119 std::memcpy(f.dest, p, n);
120 f.dest[n] = '\0';
121 at += n;
122 }
123 if (!need(18)) { *why = "truncated envelope"; return 0; }
124 for (int i = 0; i < 8; ++i) out->issued_at = (out->issued_at << 8) | b[at++];
125 for (int i = 0; i < 8; ++i) out->expires_at = (out->expires_at << 8) | b[at++];
126 const size_t plen = (size_t(b[at]) << 8) | b[at + 1];
127 at += 2;
128 if (plen > sizeof(out->params)) { *why = "params longer than 1024 bytes"; return 0; }
129 if (!need(plen)) { *why = "truncated envelope"; return 0; }
130 std::memcpy(out->params, b.data() + at, plen);
131 out->params_len = plen;
132 at += plen;
133 if (b.size() != at + 64) { *why = "missing or extra signature bytes"; return 0; }
134 return at;
135}
136
137ckey_status sa_decode(const uint8_t *payload, size_t len, std::vector<uint8_t> &bytes, ckey_safe_action *out,
138 size_t *sig_at, ckey_diagnostics *diag) {
139 if (!payload || !out) { set_status(diag, CKEY_INVALID_ARGUMENT, "payload and output are required"); return CKEY_INVALID_ARGUMENT; }
140 if (len <= kSafeActionPrefixLen || std::memcmp(payload, kSafeActionPrefix, kSafeActionPrefixLen) != 0) {
141 set_status(diag, CKEY_INVALID_ARGUMENT, "not a Safe Action (no ckey-action:1: prefix)");
142 return CKEY_INVALID_ARGUMENT;
143 }
144 if (len > kSafeActionPrefixLen + 4096 || !b64url_decode(payload + kSafeActionPrefixLen, len - kSafeActionPrefixLen, bytes)) {
145 set_status(diag, CKEY_INVALID_ARGUMENT, "Safe Action envelope is not valid base64url");
146 return CKEY_INVALID_ARGUMENT;
147 }
148 const char *why = "malformed envelope";
149 *sig_at = sa_parse_bytes(bytes, out, &why);
150 if (!*sig_at) {
151 std::memset(out, 0, sizeof(*out));
152 set_status(diag, CKEY_INVALID_ARGUMENT, (std::string("invalid Safe Action: ") + why).c_str());
153 return CKEY_INVALID_ARGUMENT;
154 }
155 return CKEY_OK;
156}
157
158} // namespace
159
160extern "C" {
161
162int ckey_is_safe_action(const uint8_t *payload, size_t len) {
163 return payload && len > kSafeActionPrefixLen && std::memcmp(payload, kSafeActionPrefix, kSafeActionPrefixLen) == 0;
164}
165
166ckey_status ckey_safe_action_parse(const uint8_t *payload, size_t len, ckey_safe_action *out, ckey_diagnostics *diag) {
167 std::vector<uint8_t> bytes;
168 size_t sig_at = 0;
169 const ckey_status st = sa_decode(payload, len, bytes, out, &sig_at, diag);
170 if (st == CKEY_OK) set_status(diag, CKEY_OK, "parsed; the signature has not been checked");
171 return st;
172}
173
174ckey_status ckey_safe_action_verify(const uint8_t *payload, size_t len, const uint8_t public_key[32], uint64_t now,
175 uint32_t max_lifetime_seconds, ckey_safe_action *out, ckey_diagnostics *diag) {
176 if (!public_key) { set_status(diag, CKEY_INVALID_ARGUMENT, "a public key is required"); return CKEY_INVALID_ARGUMENT; }
177 std::vector<uint8_t> bytes;
178 size_t sig_at = 0;
179 ckey_status st = sa_decode(payload, len, bytes, out, &sig_at, diag);
180 if (st != CKEY_OK) return st;
181 std::vector<uint8_t> message(kSafeActionContext, kSafeActionContext + sizeof(kSafeActionContext));
182 message.insert(message.end(), bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(sig_at));
183 if (!ckey_crypto::ed25519_verify(bytes.data() + sig_at, message.data(), message.size(), public_key)) {
184 std::memset(out, 0, sizeof(*out));
185 set_status(diag, CKEY_INTEGRITY_FAILURE, "the signature does not match this key");
186 return CKEY_INTEGRITY_FAILURE;
187 }
188 const uint64_t lifetime = max_lifetime_seconds ? max_lifetime_seconds : 600;
189 const uint64_t skew = 120; // tolerated clock difference, seconds
190 const char *expired = nullptr;
191 if (out->expires_at <= out->issued_at) expired = "expires before it is issued";
192 else if (out->expires_at - out->issued_at > lifetime) expired = "valid for longer than this app allows";
193 else if (now + skew < out->issued_at) expired = "issued in the future";
194 else if (now >= out->expires_at) expired = "expired";
195 if (expired) {
196 set_status(diag, CKEY_EXPIRED, (std::string("Safe Action ") + expired).c_str());
197 return CKEY_EXPIRED; // out keeps the parsed, verified fields for diagnostics
198 }
199 set_status(diag, CKEY_OK, "verified");
200 return CKEY_OK;
201}
202
203ckey_status ckey_safe_action_sign(const ckey_safe_action *fields, const uint8_t seed[32], char *out, size_t out_capacity,
204 size_t *out_len, ckey_diagnostics *diag) {
205 if (!fields || !seed) { set_status(diag, CKEY_INVALID_ARGUMENT, "fields and a signing seed are required"); return CKEY_INVALID_ARGUMENT; }
206 if (fields->scope > 1) { set_status(diag, CKEY_INVALID_ARGUMENT, "unknown scope"); return CKEY_INVALID_ARGUMENT; }
207 std::vector<uint8_t> b = {1, static_cast<uint8_t>(fields->scope)};
208 const char *strings[] = {fields->issuer, fields->app, fields->action, fields->key_id, fields->request_id};
209 const size_t caps[] = {sizeof(fields->issuer), sizeof(fields->app), sizeof(fields->action), sizeof(fields->key_id), sizeof(fields->request_id)};
210 for (int i = 0; i < 5; ++i) {
211 const size_t n = strnlen(strings[i], caps[i]);
212 if (n == caps[i] || n > 255) { set_status(diag, CKEY_INVALID_ARGUMENT, "a text field is not terminated"); return CKEY_INVALID_ARGUMENT; }
213 b.push_back(static_cast<uint8_t>(n));
214 b.insert(b.end(), strings[i], strings[i] + n);
215 }
216 for (int i = 7; i >= 0; --i) b.push_back(static_cast<uint8_t>(fields->issued_at >> (8 * i)));
217 for (int i = 7; i >= 0; --i) b.push_back(static_cast<uint8_t>(fields->expires_at >> (8 * i)));
218 if (fields->params_len > sizeof(fields->params)) { set_status(diag, CKEY_INVALID_ARGUMENT, "params longer than 1024 bytes"); return CKEY_INVALID_ARGUMENT; }
219 b.push_back(static_cast<uint8_t>(fields->params_len >> 8));
220 b.push_back(static_cast<uint8_t>(fields->params_len));
221 b.insert(b.end(), fields->params, fields->params + fields->params_len);
222 // Validate with the same parser readers use, so a signed envelope always parses.
223 {
224 std::vector<uint8_t> probe = b;
225 probe.resize(probe.size() + 64);
226 ckey_safe_action check;
227 const char *why = "malformed";
228 if (!sa_parse_bytes(probe, &check, &why)) { set_status(diag, CKEY_INVALID_ARGUMENT, (std::string("invalid Safe Action: ") + why).c_str()); return CKEY_INVALID_ARGUMENT; }
229 }
230 std::vector<uint8_t> message(kSafeActionContext, kSafeActionContext + sizeof(kSafeActionContext));
231 message.insert(message.end(), b.begin(), b.end());
232 uint8_t sig[64];
233 ckey_crypto::ed25519_sign(sig, message.data(), message.size(), seed);
234 b.insert(b.end(), sig, sig + 64);
235 std::string text(kSafeActionPrefix);
236 b64url_encode(b, text);
237 if (out_len) *out_len = text.size();
238 if (!out || out_capacity < text.size() + 1) { set_status(diag, CKEY_INSUFFICIENT_BUFFER, "output buffer too small"); return CKEY_INSUFFICIENT_BUFFER; }
239 std::memcpy(out, text.data(), text.size());
240 out[text.size()] = '\0';
241 set_status(diag, CKEY_OK, "signed");
242 return CKEY_OK;
243}
244
245void ckey_ed25519_public_key(const uint8_t seed[32], uint8_t out_public_key[32]) {
246 if (seed && out_public_key) ckey_crypto::ed25519_public_key(out_public_key, seed);
247}
248
249} // extern "C"