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

safe_actions_tests.cpp

v0.1.0-build.85 Latest Commit ede38f5

Size
8.5 KB
Lines
177
Language
cpp
SHA-256
c0d95353597c…
View on GitHub

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

1// Safe Actions: Ed25519 against RFC 8032, the envelope against vectors from an
2// independent implementation (tools/safe_actions_vectors.mjs), and sign/parse
3// round trips through the public C ABI.
4#include <ckey/ckey.h>
5
6#include <cctype>
7#include <cstdint>
8#include <cstdio>
9#include <cstring>
10#include <fstream>
11#include <map>
12#include <memory>
13#include <sstream>
14#include <string>
15#include <vector>
16
17#include "check.h"
18
19namespace {
20
21std::vector<uint8_t> hex(const std::string &s) {
22 std::vector<uint8_t> v;
23 for (size_t i = 0; i + 1 < s.size(); i += 2) v.push_back(static_cast<uint8_t>(std::stoi(s.substr(i, 2), nullptr, 16)));
24 return v;
25}
26
27// A small JSON reader: enough for the vector file (objects, arrays, strings, integers).
28struct Json {
29 enum Kind { Null, Num, Str, Arr, Obj } kind = Null;
30 long long num = 0;
31 std::string str;
32 std::vector<Json> arr;
33 std::map<std::string, Json> obj;
34 const Json &operator[](const std::string &k) const { static Json none; auto it = obj.find(k); return it == obj.end() ? none : it->second; }
35};
36
37struct Reader {
38 const std::string &s;
39 size_t i = 0;
40 void ws() { while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i; }
41 Json value() {
42 ws();
43 Json j;
44 if (s[i] == '{') {
45 j.kind = Json::Obj; ++i; ws();
46 if (s[i] == '}') { ++i; return j; }
47 while (true) { ws(); std::string k = string(); ws(); CKEY_CHECK(s[i] == ':'); ++i; j.obj[k] = value(); ws(); if (s[i] == ',') { ++i; continue; } CKEY_CHECK(s[i] == '}'); ++i; return j; }
48 }
49 if (s[i] == '[') {
50 j.kind = Json::Arr; ++i; ws();
51 if (s[i] == ']') { ++i; return j; }
52 while (true) { j.arr.push_back(value()); ws(); if (s[i] == ',') { ++i; continue; } CKEY_CHECK(s[i] == ']'); ++i; return j; }
53 }
54 if (s[i] == '"') { j.kind = Json::Str; j.str = string(); return j; }
55 j.kind = Json::Num;
56 size_t end = i;
57 while (end < s.size() && (std::isdigit(static_cast<unsigned char>(s[end])) || s[end] == '-')) ++end;
58 j.num = std::stoll(s.substr(i, end - i));
59 i = end;
60 return j;
61 }
62 std::string string() {
63 CKEY_CHECK(s[i] == '"'); ++i;
64 std::string out;
65 while (s[i] != '"') {
66 if (s[i] == '\\') { ++i; out.push_back(s[i] == 'n' ? '\n' : s[i]); ++i; }
67 else out.push_back(s[i++]);
68 }
69 ++i;
70 return out;
71 }
72};
73
74void test_rfc8032() {
75 // RFC 8032 section 7.1, tests 1 and 2, through the public key derivation and the envelope signer's key.
76 uint8_t pk[32];
77 ckey_ed25519_public_key(hex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60").data(), pk);
78 CKEY_CHECK(std::memcmp(pk, hex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a").data(), 32) == 0);
79 ckey_ed25519_public_key(hex("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb").data(), pk);
80 CKEY_CHECK(std::memcmp(pk, hex("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c").data(), 32) == 0);
81}
82
83void test_vectors(const char *path) {
84 std::ifstream f(path);
85 CKEY_CHECK(f.good());
86 std::stringstream ss;
87 ss << f.rdbuf();
88 const std::string text = ss.str();
89 Reader r{text};
90 const Json root = r.value();
91 CKEY_CHECK(root["schema"].num == 1);
92 int checked = 0;
93 for (const Json &v : root["vectors"].arr) {
94 const std::string &payload = v["payload"].str;
95 const std::vector<uint8_t> key = hex(v["public_key"].str);
96 auto out = std::make_unique<ckey_safe_action>();
97 ckey_diagnostics diag{};
98 const ckey_status st = ckey_safe_action_verify(reinterpret_cast<const uint8_t *>(payload.data()), payload.size(), key.data(),
99 static_cast<uint64_t>(v["now"].num), static_cast<uint32_t>(v["max_lifetime"].num), out.get(), &diag);
100 const std::string &expect = v["expect"].str;
101 const bool ok = (expect == "ok" && st == CKEY_OK) || (expect == "integrity" && st == CKEY_INTEGRITY_FAILURE) ||
102 (expect == "expired" && st == CKEY_EXPIRED) || (expect == "invalid" && st == CKEY_INVALID_ARGUMENT) ||
103 (expect == "invalid_or_integrity" && (st == CKEY_INVALID_ARGUMENT || st == CKEY_INTEGRITY_FAILURE));
104 if (!ok) std::fprintf(stderr, "vector \"%s\": expected %s, got status %d (%s)\n", v["name"].str.c_str(), expect.c_str(), st, diag.message);
105 CKEY_CHECK(ok);
106 if (expect == "ok") {
107 const Json &fields = v["fields"];
108 CKEY_CHECK(out->scope == static_cast<uint32_t>(fields["scope"].num));
109 CKEY_CHECK(fields["issuer"].str == out->issuer && fields["app"].str == out->app && fields["action"].str == out->action);
110 CKEY_CHECK(fields["key_id"].str == out->key_id && fields["request_id"].str == out->request_id);
111 CKEY_CHECK(out->issued_at == static_cast<uint64_t>(fields["issued_at"].num) && out->expires_at == static_cast<uint64_t>(fields["expires_at"].num));
112 CKEY_CHECK(fields["params"].str == std::string(reinterpret_cast<const char *>(out->params), out->params_len));
113 }
114 ++checked;
115 }
116 CKEY_CHECK(checked >= 20);
117 std::printf("safe actions: %d vectors\n", checked);
118}
119
120void test_round_trip() {
121 const std::vector<uint8_t> seed = hex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
122 uint8_t pk[32];
123 ckey_ed25519_public_key(seed.data(), pk);
124 auto fields = std::make_unique<ckey_safe_action>();
125 fields->scope = CKEY_SCOPE_CROSS_DEVICE;
126 std::strcpy(fields->issuer, "citeory.com");
127 std::strcpy(fields->app, "com.viadere");
128 std::strcpy(fields->action, "signin.request");
129 std::strcpy(fields->key_id, "2026-11");
130 std::strcpy(fields->request_id, "sess-0123456789ab");
131 fields->issued_at = 1794000000;
132 fields->expires_at = 1794000060;
133 const char params[] = "{\"return\":\"/dashboard\"}";
134 std::memcpy(fields->params, params, sizeof(params) - 1);
135 fields->params_len = sizeof(params) - 1;
136 size_t need = 0;
137 ckey_diagnostics diag{};
138 CKEY_CHECK(ckey_safe_action_sign(fields.get(), seed.data(), nullptr, 0, &need, &diag) == CKEY_INSUFFICIENT_BUFFER);
139 std::vector<char> text(need + 1);
140 CKEY_CHECK(ckey_safe_action_sign(fields.get(), seed.data(), text.data(), text.size(), &need, &diag) == CKEY_OK);
141 const auto *bytes = reinterpret_cast<const uint8_t *>(text.data());
142 CKEY_CHECK(ckey_is_safe_action(bytes, need) == 1);
143 CKEY_CHECK(ckey_is_safe_action(reinterpret_cast<const uint8_t *>("https://x"), 9) == 0);
144 auto parsed = std::make_unique<ckey_safe_action>();
145 CKEY_CHECK(ckey_safe_action_parse(bytes, need, parsed.get(), &diag) == CKEY_OK);
146 CKEY_CHECK(std::strcmp(parsed->action, "signin.request") == 0 && parsed->scope == CKEY_SCOPE_CROSS_DEVICE);
147 auto verified = std::make_unique<ckey_safe_action>();
148 CKEY_CHECK(ckey_safe_action_verify(bytes, need, pk, 1794000030, 600, verified.get(), &diag) == CKEY_OK);
149 CKEY_CHECK(std::memcmp(verified->params, params, sizeof(params) - 1) == 0);
150 CKEY_CHECK(ckey_safe_action_verify(bytes, need, pk, 1794000061, 600, verified.get(), &diag) == CKEY_EXPIRED);
151 CKEY_CHECK(ckey_safe_action_verify(bytes, need, pk, 1794000030, 30, verified.get(), &diag) == CKEY_EXPIRED);
152 // Every single-character change to the envelope is refused.
153 const size_t prefix = std::strlen("ckey-action:1:");
154 int refused = 0;
155 for (size_t i = prefix; i < need; ++i) {
156 std::string changed(text.data(), need);
157 changed[i] = changed[i] == 'A' ? 'B' : 'A';
158 if (ckey_safe_action_verify(reinterpret_cast<const uint8_t *>(changed.data()), changed.size(), pk, 1794000030, 600, verified.get(), &diag) != CKEY_OK) ++refused;
159 }
160 CKEY_CHECK(refused == static_cast<int>(need - prefix));
161 // Fields the format cannot carry are refused at signing time.
162 std::strcpy(fields->issuer, "Not A Domain");
163 CKEY_CHECK(ckey_safe_action_sign(fields.get(), seed.data(), text.data(), text.size(), &need, &diag) == CKEY_INVALID_ARGUMENT);
164 std::strcpy(fields->issuer, "citeory.com");
165 fields->params_len = 2000;
166 CKEY_CHECK(ckey_safe_action_sign(fields.get(), seed.data(), text.data(), text.size(), &need, &diag) == CKEY_INVALID_ARGUMENT);
167 std::puts("safe actions: round trip, tamper and field checks");
168}
169
170} // namespace
171
172int main(int argc, char **argv) {
173 test_rfc8032();
174 test_round_trip();
175 test_vectors(argc > 1 ? argv[1] : "tests/vectors/safe-actions-v1.json");
176 return 0;
177}