pub struct Order {Show 18 fields
pub id: String,
pub profile: String,
pub account_id: String,
pub status: OrderStatus,
pub identifiers: Vec<Identifier>,
pub expires: i64,
pub not_before: Option<i64>,
pub not_after: Option<i64>,
pub error: Option<Value>,
pub certificate: Option<String>,
pub replaces: Option<String>,
pub cert_serial: Option<String>,
pub cert_pubkey: Option<Vec<u8>>,
pub revoked_at: Option<i64>,
pub revocation_reason: Option<i64>,
pub created_at: i64,
pub created_ip: Option<String>,
pub created_ptr: Option<String>,
}Expand description
An ACME order (RFC 8555 §7.1.3). A new order is created in the pending
state with one authorization per identifier; once every authorization is
valid (its http-01 challenge triggered) the order moves to ready and is
finalizable.
§Storage Details
identifiersis persisted as a JSON array of{type, value}objects.erroris a nullable JSON problem document (set if issuance fails).certificateholds the issued PEM chain, null until finalized.cert_serial/cert_pubkeyare populated alongsidecertificate(byOrder::finalize) from the leaf’s own serial (hex) and DER-SPKI public key — the former is how aPOST /revokeCertrequest is looked up (Order::find_by_cert_serial), the latter is how it can be authorized by the certificate’s own key pair (RFC 8555 §7.6’s accountless case).revoked_at/revocation_reasonare this order’s own revocation bookkeeping (Order::revoke), orthogonal tostatus: RFC 8555 defines no “revoked” order status, so a revoked order’sstatusstaysvalid.- Timestamps are epoch seconds, matching accounts/nonces, and rendered as
RFC3339 datetime strings in
Order::to_json. - The
authorizationsURLs are derived from the order’s authorization ids (looked up separately and passed intoOrder::to_json); thefinalize/certificateURLs are derived from the id + base URL, never stored (likeAccount’sordersURL).
Fields§
§id: String§profile: StringThe ACME endpoint ([profiles.<name>]) this order was placed at. It
always matches the owning account’s own profile — the redundancy is
what lets the two lookups that take no account (find_by_cert_serial,
for revocation, and ARI) stay scoped to one endpoint.
account_id: String§status: OrderStatus§identifiers: Vec<Identifier>§expires: i64§not_before: Option<i64>§not_after: Option<i64>§error: Option<Value>§certificate: Option<String>§replaces: Option<String>The RFC 9773 §5 certID of the certificate this order is meant to
replace, when the client named one. Reflected back in Order::to_json
because §5 requires it: “If the server accepts a newOrder request with a
replaces field, it MUST reflect that field in the response and in
subsequent requests for the corresponding Order object.”
cert_serial: Option<String>§cert_pubkey: Option<Vec<u8>>§revoked_at: Option<i64>§revocation_reason: Option<i64>§created_at: i64§created_ip: Option<String>Where newOrder was called from, and the reverse name that address had
at the time. Traceability only, never compared, and deliberately never
rendered by Order::to_json — see the schema comment in
migrations/20260725120000_add_orders.sql. There is no update-side
pair: the moment that matters after creation is issuance, which is an
audit_log row carrying its own address.
created_ptr: Option<String>Implementations§
Source§impl Order
impl Order
Sourcepub async fn create(
profile: &str,
account_id: &str,
identifiers: Vec<Identifier>,
expires: i64,
not_before: Option<i64>,
not_after: Option<i64>,
database: &Database,
) -> Result<Order, Error>
pub async fn create( profile: &str, account_id: &str, identifiers: Vec<Identifier>, expires: i64, not_before: Option<i64>, not_after: Option<i64>, database: &Database, ) -> Result<Order, Error>
Creates a new order in the pending state (its authorizations are created
separately by the caller) and returns it.
pub async fn find_by_id( id: &str, database: &Database, ) -> Result<Option<Order>, Error>
Sourcepub async fn find_by_account(
account_id: &str,
database: &Database,
) -> Result<Vec<Order>, Error>
pub async fn find_by_account( account_id: &str, database: &Database, ) -> Result<Vec<Order>, Error>
Every order belonging to an account, newest first.
Unfiltered on purpose: the admin CLI counts these to tell an operator
what a DELETE will cascade, and a filtered count would understate it.
The client-facing order-list URL wants
Order::find_active_by_account instead.
Sourcepub async fn find_active_by_account(
account_id: &str,
database: &Database,
) -> Result<Vec<Order>, Error>
pub async fn find_active_by_account( account_id: &str, database: &Database, ) -> Result<Vec<Order>, Error>
An account’s orders that are still worth a client’s attention, newest first — what the RFC 8555 §7.1.2.1 order-list URL serves.
§7.1.2.1: “The server SHOULD include pending orders and SHOULD NOT
include orders that are invalid in the array of URLs.” Expired orders go
too: load_owned_order refuses one unless it is already valid, so
listing it would hand the client a URL that only ever answers with an
error.
valid orders are kept whatever their expires, since the order
object’s expiry is housekeeping (order.validity_seconds) and the
certificate it points at outlives it — that URL still works.
Sourcepub async fn list_all(
profile: Option<&str>,
database: &Database,
) -> Result<Vec<Order>, Error>
pub async fn list_all( profile: Option<&str>, database: &Database, ) -> Result<Vec<Order>, Error>
Lists orders across every account, oldest first — the admin CLI’s
listing. profile filters to one endpoint (None lists all of them);
unlike Order::find_by_account (one account, newest-first, for the
order-list URL) there is no account filter — callers filter by
account/status client-side rather than building dynamic SQL for what is
expected to be a small, locally-run admin table.
Sourcepub async fn search(
query: &OrderQuery,
database: &Database,
) -> Result<(Vec<Order>, i64), Error>
pub async fn search( query: &OrderQuery, database: &Database, ) -> Result<(Vec<Order>, i64), Error>
One page of orders matching query, plus the total the same predicate
matches unpaged.
Additive: Order::list_all is untouched, because the admin CLI counts
on getting everything. This exists because orders grows a row per
issuance forever — the first operator to open the web admin on a
year-old deployment would otherwise pull the whole table into memory and
JSON-encode it.
The filters are in SQL rather than applied afterwards for the same
reason they have to be: filtering a page in memory would make the page
size wrong. It is also the only implementation of that policy — the
CLI’s order list used to hold a second one in Rust over list_all,
which meant one meaning of --status written twice and a whole table
loaded to filter three fields.
Built with a sqlx::QueryBuilder: sqlx::query takes only
&'static str, and every value below goes through push_bind, so
nothing operator- or client-supplied is ever interpolated into the SQL.
Sourcepub async fn count_by_account(
account_id: &str,
database: &Database,
) -> Result<i64, Error>
pub async fn count_by_account( account_id: &str, database: &Database, ) -> Result<i64, Error>
How many orders an account has.
COUNT(*), not find_by_account(..).len(): the only two callers want a
number, and loading every row means deserializing each one’s
identifiers JSON to throw it away.
Sourcepub async fn delete(id: &str, database: &Database) -> Result<bool, Error>
pub async fn delete(id: &str, database: &Database) -> Result<bool, Error>
Hard-deletes the order row — cascading, via ON DELETE CASCADE, to its
authorizations and challenges. Returns whether a row existed to delete.
Sourcepub async fn finalize(
&mut self,
chain: String,
cert_serial: String,
cert_pubkey: Vec<u8>,
database: &Database,
) -> Result<(), Error>
pub async fn finalize( &mut self, chain: String, cert_serial: String, cert_pubkey: Vec<u8>, database: &Database, ) -> Result<(), Error>
Records a successful issuance: stores the PEM chain plus the leaf’s
cert_serial (hex) and cert_pubkey (DER SPKI) — populated by the
caller from that same chain via crate::cert::cert_serial_and_spki,
since parsing needs error handling the DB layer doesn’t otherwise deal
in — moves the order to the terminal valid state, and keeps self
in sync so a following Order::to_json reflects the change without
a re-read.
Sourcepub async fn find_by_cert_serial(
profile: &str,
serial: &str,
database: &Database,
) -> Result<Option<Order>, Error>
pub async fn find_by_cert_serial( profile: &str, serial: &str, database: &Database, ) -> Result<Option<Order>, Error>
Looks up the order whose stored certificate carries serial (hex,
matching cert_serial’s format) — indexed, so a POST /revokeCert
request is not a full table scan across every issued order. Not
proof of identity on its own: the caller must additionally compare the
submitted certificate’s DER against the returned order’s stored chain
byte-for-byte (a random-serial collision, or a crafted certificate
reusing a real serial, are not ruled out by the serial alone).
Scoped to profile: revocation and ARI carry no account, so the
endpoint the request arrived at is the only thing that keeps one
profile from answering for — or revoking — another’s certificate.
Sourcepub async fn find_by_replaces(
profile: &str,
cert_id: &str,
database: &Database,
) -> Result<Option<Order>, Error>
pub async fn find_by_replaces( profile: &str, cert_id: &str, database: &Database, ) -> Result<Option<Order>, Error>
Finds any order that already claims to replace cert_id and is not
invalid — RFC 9773 §5’s “the identified certificate has not already
been marked as replaced by a different Order that is not invalid”.
The invalid exclusion is what makes a retry work: an order that failed
validation never produced a replacement, so it must not hold the
predecessor hostage. Scoped by profile like every other request-path
lookup — a certID is only meaningful at the endpoint that issued it.
Sourcepub async fn revoke(
&mut self,
reason: Option<i64>,
database: &Database,
) -> Result<(), Error>
pub async fn revoke( &mut self, reason: Option<i64>, database: &Database, ) -> Result<(), Error>
Records a certificate revocation (RFC 8555 §7.6): stamps revoked_at
(now) and the optional CRLReason reason, and keeps self in sync
(like Order::finalize). Deliberately does not touch status:
revocation is orthogonal to the order state machine (RFC 8555 defines
no “revoked” order status), so a revoked order stays valid.
pub async fn mark_invalid( &mut self, error: Value, database: &Database, ) -> Result<(), Error>
Sourcepub async fn mark_ready(&mut self, database: &Database) -> Result<(), Error>
pub async fn mark_ready(&mut self, database: &Database) -> Result<(), Error>
Moves the order from pending to ready once all its authorizations are
valid, so it can be finalized. Keeps self in sync (like
Order::finalize).
Sourcepub async fn mark_pending(&mut self, database: &Database) -> Result<(), Error>
pub async fn mark_pending(&mut self, database: &Database) -> Result<(), Error>
Moves the order back from ready to pending, after one of its
authorizations stopped being valid — in practice, a client
deactivating one (RFC 8555 §7.5.2).
The only backwards transition in the order state machine, and it exists
because §7.5.2’s “the server MUST NOT treat deactivated authorization
objects as sufficient for issuing certificates” has to hold for an order
that already reached ready — otherwise finalize would still accept
it. RFC 8555 §7.1.6’s diagram draws pending → ready as the state
becoming true rather than a one-way latch, so re-deriving it is in
keeping with the model.
Sourcepub async fn claim_for_finalize(
&mut self,
database: &Database,
) -> Result<bool, Error>
pub async fn claim_for_finalize( &mut self, database: &Database, ) -> Result<bool, Error>
Claims the order for issuance, moving it from ready to processing in
one guarded statement: Ok(true) means this caller won the claim,
Ok(false) that somebody else already holds it. Keeps self in sync
(like Order::mark_ready) only on the winning branch.
The precondition is the whole point. post_finalize reads the order,
checks it is ready, signs, and writes — three steps with no lock
between them, so N concurrent finalize requests on one order all passed
the check, all reached SignerBackend::issue, and all got a certificate
back. Only the last write survived, and the others became valid
CA-signed certificates with no row naming their serial: POST /revokeCert looks an order up by find_by_cert_serial and would answer
“unknown certificate”, so nothing this server offers could ever revoke
them and the CRL would never learn they exist. rows_affected closes it,
the primitive crate::sqlite::nonce::Nonce::verify and
AdminUser::claim_totp_step already rest on.
The relay backend was never exposed, because upstream_orders.order_id
is a primary key and the second insert conflicts — this gives local_ca
and custom, which answer inline, the same guard.
The processing status needed no migration — the orders.status
CHECK has always allowed it; until the relay backend existed
there was simply no asynchronous issuance to use it.
Sourcepub async fn release_finalize_claim(
&mut self,
database: &Database,
) -> Result<(), Error>
pub async fn release_finalize_claim( &mut self, database: &Database, ) -> Result<(), Error>
Gives the claim back, moving processing to ready so the client can
try again — the counterpart to Order::claim_for_finalize for the
refusals RFC 8555 §7.4 says must leave the order finalizable (a rejected
CSR) and for the two arms where issuance succeeded but this server could
not read what it had just been handed.
Guarded on processing for a reason of its own: a §7.5.2 deactivation
racing this claim demotes the order to pending
(Order::mark_pending, unguarded, since it is the authoritative
answer to an authorization that stopped being valid). An unguarded
release would push it back to ready and hand the client a finalizable
order whose authorizations no longer support it.
Sourcepub fn to_json(&self, base_url: &str, authz_ids: &[String]) -> Value
pub fn to_json(&self, base_url: &str, authz_ids: &[String]) -> Value
The RFC 8555 order object. URLs are derived from base_url; datetimes are
rendered RFC3339. authorizations lists one URL per authz_ids entry, the
certificate URL appears only once the order is valid, and notBefore/
notAfter/error appear only when set.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Order
impl RefUnwindSafe for Order
impl Send for Order
impl Sync for Order
impl Unpin for Order
impl UnsafeUnpin for Order
impl UnwindSafe for Order
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more