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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
//! The `crls` table: each local CA's current signed CRL.
//!
//! One row per issuer, replaced in place. RFC 5280 §5.2.3's `crlNumber` must
//! only increase, and several processes may sign a CRL for one CA, so a
//! replacement is guarded on the number its writer read:
//! [`StoredCrl::replace_if_number`] answers `false` to a writer that lost the
//! race, which re-reads and signs again. Nothing here signs anything — that is
//! `signer::local_ca`'s job, done outside any transaction since a PKCS#11
//! signature is a token round trip.
use tracing::{debug, info};
use crate::sql::Row;
use crate::db::Database;
/// A signed CRL as stored.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredCrl {
/// Hex SHA-256 of the issuing CA's SubjectPublicKeyInfo.
pub issuer: String,
pub crl_number: u64,
pub der: Vec<u8>,
/// Epoch seconds, as signed into the CRL.
pub this_update: i64,
/// Epoch seconds, as signed into the CRL.
pub next_update: i64,
}
impl StoredCrl {
fn from_row(row: &Row) -> Result<Self, sqlx::Error> {
let number: i64 = row.try_get("crl_number")?;
Ok(Self {
issuer: row.try_get("issuer")?,
crl_number: u64::try_from(number).map_err(|error| sqlx::Error::ColumnDecode {
index: "crl_number".to_string(),
source: Box::new(error),
})?,
der: row.try_get("der")?,
this_update: row.try_get("this_update")?,
next_update: row.try_get("next_update")?,
})
}
/// The current CRL for `issuer`, if one has been stored.
pub async fn find<'e>(
issuer: &str,
executor: impl Into<crate::sql::Exec<'e>>,
) -> Result<Option<Self>, sqlx::Error> {
crate::sql::query(
"SELECT issuer, crl_number, der, this_update, next_update FROM crls WHERE issuer = ?;",
)
.bind(issuer)
.fetch_optional(executor)
.await?
.as_ref()
.map(Self::from_row)
.transpose()
}
/// [`find`](Self::find) over the pool, for a reader outside `crates/store/src/`
/// that holds a [`Database`] rather than a
/// connection — the read side of a local CA, which serves the stored CRL
/// and never signs one.
pub async fn find_current(
issuer: &str,
database: &Database,
) -> Result<Option<Self>, sqlx::Error> {
Self::find(issuer, database).await
}
/// Stores the first CRL for its issuer, answering whether it was written.
///
/// `false` means another writer stored one first, and theirs stands. This
/// is what makes a CA's one-time initialisation — the sidecar import — safe
/// to race: whoever inserts this row owns the import, in the same
/// transaction.
pub async fn insert_initial<'e>(
&self,
executor: impl Into<crate::sql::Exec<'e>>,
) -> Result<bool, sqlx::Error> {
let result = crate::sql::query(
"INSERT INTO crls (issuer, crl_number, der, this_update, next_update) \
VALUES (?, ?, ?, ?, ?) ON CONFLICT (issuer) DO NOTHING;",
)
.bind(&self.issuer)
.bind(number(self.crl_number)?)
.bind(&self.der)
.bind(self.this_update)
.bind(self.next_update)
.execute(executor)
.await?;
let inserted = result.rows_affected() == 1;
info!(
event = "db_crl_initialized",
outcome = "success",
issuer = %self.issuer,
crl_number = self.crl_number,
inserted,
);
Ok(inserted)
}
/// Replaces the stored CRL with `self`, but only if the stored one is still
/// numbered `expected` — the number this writer read before signing.
///
/// `false` means somebody else stored a CRL in between. The caller must not
/// retry this same `self`, whose snapshot is now older than what is stored;
/// it re-reads and signs again.
pub async fn replace_if_number(
&self,
expected: u64,
database: &Database,
) -> Result<bool, sqlx::Error> {
let result = crate::sql::query(
"UPDATE crls SET crl_number = ?, der = ?, this_update = ?, next_update = ? \
WHERE issuer = ? AND crl_number = ?;",
)
.bind(number(self.crl_number)?)
.bind(&self.der)
.bind(self.this_update)
.bind(self.next_update)
.bind(&self.issuer)
.bind(number(expected)?)
.execute(database)
.await?;
let replaced = result.rows_affected() == 1;
if replaced {
info!(
event = "db_crl_replaced",
outcome = "success",
issuer = %self.issuer,
crl_number = self.crl_number,
);
} else {
debug!(
event = "db_crl_replace_superseded",
outcome = "failure",
issuer = %self.issuer,
expected_crl_number = expected,
);
}
Ok(replaced)
}
}
/// A `crl_number` as SQLite stores it. `u64` past `i64::MAX` cannot be bound,
/// and cannot occur: it is a counter bumped once per signed CRL.
fn number(value: u64) -> Result<i64, sqlx::Error> {
i64::try_from(value).map_err(|error| sqlx::Error::Encode(Box::new(error)))
}
#[cfg(test)]
mod tests {
use super::*;
fn crl(number: u64, der: &[u8]) -> StoredCrl {
StoredCrl {
issuer: "ca".to_string(),
crl_number: number,
der: der.to_vec(),
this_update: 10,
next_update: 20,
}
}
#[tokio::test]
async fn the_first_initial_crl_wins() {
let database = Database::connect_for_test().await.unwrap();
assert!(StoredCrl::find("ca", &database).await.unwrap().is_none());
assert!(crl(1, b"first").insert_initial(&database).await.unwrap());
assert!(!crl(7, b"second").insert_initial(&database).await.unwrap());
assert_eq!(
StoredCrl::find("ca", &database).await.unwrap(),
Some(crl(1, b"first"))
);
}
/// The guard that keeps `crl_number` monotonic across writers: a CRL signed
/// over a snapshot that is no longer current is refused, not stored.
#[tokio::test]
async fn a_replacement_signed_over_a_stale_number_is_refused() {
let database = Database::connect_for_test().await.unwrap();
crl(1, b"one").insert_initial(&database).await.unwrap();
// Two writers both read number 1; the first to store wins.
assert!(
crl(2, b"two-a")
.replace_if_number(1, &database)
.await
.unwrap()
);
assert!(
!crl(2, b"two-b")
.replace_if_number(1, &database)
.await
.unwrap()
);
// The loser re-reads, and its next attempt lands above the winner.
assert!(
crl(3, b"three")
.replace_if_number(2, &database)
.await
.unwrap()
);
assert_eq!(
StoredCrl::find("ca", &database).await.unwrap(),
Some(crl(3, b"three"))
);
}
#[tokio::test]
async fn nothing_is_replaced_for_an_issuer_never_initialised() {
let database = Database::connect_for_test().await.unwrap();
assert!(
!crl(2, b"two")
.replace_if_number(1, &database)
.await
.unwrap()
);
assert!(StoredCrl::find("ca", &database).await.unwrap().is_none());
}
}