1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! **The key the appliance signs an actor ticket with.**
//!
//! An actor ticket is the appliance's word that one human may act for one
//! tenant; `Billing.MintActorTicket` makes one and the other side of the
//! monetize seam checks the signature offline, against the public half this
//! box advertises. That arrangement has exactly two failure modes worth a test
//! file, and both are about the key rather than about the ticket:
//!
//! * **the key must be stable.** It is minted on first use and kept in the
//! secret store. If a reboot minted a new one, every ticket this appliance
//! had ever advertised a key for would stop verifying, and the symptom would
//! be "the renewal page is broken" rather than anything naming a key.
//! * **the key must be its own.** The obvious shortcut — sign with the SSH host
//! key, which every appliance already has — is a cross-protocol reuse with no
//! domain separation at the signature layer. `gunnar_store::ACTOR_TICKET_KEY`
//! says at length why that was not taken; this file is what holds the answer
//! to the code.
//!
//! The target is `required-features = ["metered"]`: the community appliance
//! mints nothing under this name, because it does not contain the service that
//! would spend it.
use std::sync::Arc;
use gunnar_grpc::ControlPlane as _;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
/// **Minted once, and the same bytes ever after.**
///
/// Two `ServerStores` over one root, because that is what a restart is. The
/// second call must read the first call's key rather than mint a second one:
/// `SecretStoreExt::get_or_create_with` is what makes that atomic, and this is
/// the assertion that says we are really using it.
#[test]
fn the_actor_ticket_key_is_minted_once_and_survives_a_restart() {
let dir = tempfile::tempdir().expect("tempdir");
let first = {
let stores = ServerStores::open(dir.path()).expect("stores");
stores
.actor_ticket_key()
.expect("the first call mints a key")
};
let second = {
let stores = ServerStores::open(dir.path()).expect("stores");
stores
.actor_ticket_key()
.expect("the second call must find the first call's key")
};
assert_eq!(
first.public_key_hex(),
second.public_key_hex(),
"the appliance minted a SECOND actor-ticket key on restart. Every ticket signed by \
the first one stops verifying against the key this box now advertises, and nothing \
in the failure says the word 'key'"
);
// The public half is what an operator copies into a configuration by hand,
// so its shape is part of the contract and not an implementation detail.
assert_eq!(first.public_key_hex().len(), 64);
assert!(
first
.public_key_hex()
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"the advertised key must be lowercase hex: {}",
first.public_key_hex()
);
}
/// **It is not the SSH host key.**
///
/// The one assertion that would go red if somebody ever "saved a key" by
/// reusing the one the box already has. See `gunnar_store::ACTOR_TICKET_KEY`
/// for why that saving is not worth what it costs.
#[test]
fn the_actor_ticket_key_is_not_the_ssh_host_key() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = ServerStores::open(dir.path()).expect("stores");
let ticket_key = stores.actor_ticket_key().expect("actor-ticket key");
let host = gunnar_core::ensure_host_key(&stores.secrets().expect("secrets")).expect("host key");
let host_hex = match host.private_key().public_key().key_data().ed25519() {
Some(public) => public.0.iter().map(|b| format!("{b:02x}")).collect(),
None => String::new(),
};
assert_ne!(
ticket_key.public_key_hex(),
host_hex,
"the actor-ticket key IS the SSH host key: one key, two signature formats, and only \
one of them has a domain separator"
);
}
/// **The signature this key makes is the one the seam verifies.**
///
/// Not a ticket — that is `gunnar-grpc/tests/billing.rs`'s job — but the
/// primitive under it: the bytes `sign` returns must check out under the key
/// `public_key_hex` advertises, decoded the way an operator's configuration
/// would decode it. A mismatch here would make every ticket unverifiable while
/// every test that only re-checked with the same object stayed green.
#[test]
fn what_this_key_signs_verifies_under_the_hex_it_advertises() {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let dir = tempfile::tempdir().expect("tempdir");
let stores = ServerStores::open(dir.path()).expect("stores");
let key = stores.actor_ticket_key().expect("actor-ticket key");
let message = b"the appliance's word, as an operator's monetize would read it";
let signature = key.sign(message);
let mut raw = [0u8; 32];
for (slot, pair) in raw
.iter_mut()
.zip(key.public_key_hex().as_bytes().chunks(2))
{
*slot = u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16)
.expect("hex digit pair");
}
let verifying = VerifyingKey::from_bytes(&raw).expect("a verifying key");
verifying
.verify(message, &Signature::from_bytes(&signature))
.expect("what the appliance signed must verify under what the appliance advertises");
// And the negative half, so the assertion above is not satisfied by a
// verifier that accepts everything.
assert!(
verifying
.verify(b"other bytes", &Signature::from_bytes(&signature))
.is_err(),
"the verifier accepted a signature over other bytes"
);
}
/// **The REAL control plane answers both halves of the seam.**
///
/// `MemoryControlPlane` in `gunnar-grpc/tests/billing.rs` proves the service
/// enforces the scope; it proves nothing about the appliance that actually
/// ships, whose `ControlPlane` is this one. The default in the seam is
/// `Unimplemented` by name — the right answer for a plane with no secret store
/// — so without this test a metered appliance could serve `Billing` and refuse
/// every call, with every test in the tree green.
///
/// Both halves are checked against each other rather than separately: what the
/// plane SIGNS must verify under what the plane ADVERTISES, because two
/// methods reading two different keys is the one failure that no single-method
/// test can see.
#[test]
fn the_real_control_plane_signs_and_advertises_the_same_key() {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
let hex = plane
.actor_ticket_public_key_hex()
.expect("the shipping appliance must advertise its actor-ticket key");
assert_eq!(hex.len(), 64);
let message = b"what an appliance signs is what an appliance advertises";
let signature = plane
.actor_ticket_sign(message)
.expect("the shipping appliance must sign");
let mut raw = [0u8; 32];
for (slot, pair) in raw.iter_mut().zip(hex.as_bytes().chunks(2)) {
*slot = u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16)
.expect("hex digit pair");
}
VerifyingKey::from_bytes(&raw)
.expect("the advertised key parses")
.verify(message, &Signature::from_bytes(&signature))
.expect(
"the appliance signed under a key it does not advertise. Every ticket it mints is \
unverifiable and nothing on this box would say so",
);
// The key is the STORE's, not the plane's: a second plane over the same
// root advertises the same key, which is what survives a restart.
let second = CoreControlPlane::new(stores);
assert_eq!(
second.actor_ticket_public_key_hex().expect("advertised"),
hex
);
}