Skip to main content

Order

Struct Order 

Source
pub struct Order {
Show 19 fields pub id: Uuid, pub profile: String, pub account_id: Uuid, 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 cert_not_after: Option<i64>, 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

  • identifiers is persisted as a JSON array of {type, value} objects.
  • error is a nullable JSON problem document (set if issuance fails).
  • certificate holds the issued PEM chain, null until finalized.
  • cert_serial/cert_pubkey are populated alongside certificate (by Order::finalize) from the leaf’s own serial (hex) and DER-SPKI public key — the former is how a POST /revokeCert request 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).
  • cert_not_after is populated the same way and from the same leaf, and is not not_after: that one is the validity the client asked for (§7.4), usually absent and clamped by the signer when it is not, where this is what the certificate says. NULL means the row predates the column and nothing has parsed its chain yet; a negative value means the sweep parsed it and could not (see the migration).
  • revoked_at/revocation_reason are this order’s own revocation bookkeeping (Order::revoke), orthogonal to status: RFC 8555 defines no “revoked” order status, so a revoked order’s status stays valid.
  • Timestamps are epoch seconds, matching accounts/nonces, and rendered as RFC3339 datetime strings in Order::to_json.
  • The authorizations URLs are derived from the order’s authorization ids (looked up separately and passed into Order::to_json); the finalize/ certificate URLs are derived from the id + base URL, never stored (like Account’s orders URL).

Fields§

§id: Uuid§profile: String

The 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: Uuid§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>>§cert_not_after: Option<i64>

The leaf’s own notAfter, epoch seconds. None on a row finalized before the column existed (the digest’s backfill stamps those), and UNPARSABLE_NOT_AFTER once the backfill has looked and failed.

§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 crates/store/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

Source

pub fn new( profile: &str, account_id: Uuid, identifiers: Vec<Identifier>, expires: i64, not_before: Option<i64>, not_after: Option<i64>, ) -> Order

Builds a new order in the pending state. Pure — nothing is persisted until Order::insert runs.

Source

pub fn with_client(self, client: &ClientContext) -> Order

Records where the order was placed from.

Consuming rather than &mut self so it chains off Order::new at the one call site that has a request behind it. An order created without it — every test fixture, and any future path with no client — simply keeps two NULLs, which is the honest answer.

Source

pub async fn insert<'e>( &self, executor: impl Into<Exec<'e>>, ) -> Result<(), Error>

Inserts the order using any executor — a pool, or a transaction.

Split from Order::new so post_new_order can write the order and its authorizations inside one transaction: a half-built order (fewer authorizations than identifiers) would otherwise be finalizable for names that were never authorized.

Source

pub async fn create( profile: &str, account_id: Uuid, 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.

Source

pub async fn find_by_id( id: &str, database: &Database, ) -> Result<Option<Order>, Error>

Looks an order up by id, in any profile. An id that does not parse as one is None, like an unknown one: it came from a URL or a command line.

Source

pub async fn find_by_account( account_id: Uuid, 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.

Source

pub async fn find_active_by_account( account_id: Uuid, 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.

Source

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.

The only cross-account listing this model offers. An unpaged list_all stood beside it, oldest first, until both front ends took a window; orders grows a row per issuance forever, so an operator opening either surface on a year-old deployment would otherwise pull the whole table into memory and render 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 the unpaged listing, which meant one meaning of --status written twice and a whole table loaded to filter three fields.

Built with a crate::sql::Builder: sql::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.

Source

pub async fn cleanup( profile: &str, cutoff: i64, database: &Database, ) -> Result<u64, Error>

Deletes this profile’s orders that expired before cutoff, returning how many went. The authorizations and challenges beneath them go with the row, through the schema’s ON DELETE CASCADE.

valid is excluded, whatever the age. A valid order’s row is how Order::find_by_cert_serial resolves a certificate for revokeCert and for the CRL, and what RFC 9773 renewal information is derived from — deleting one would make an issued certificate unrevokable and unrenewable, which is a far worse outcome than a large table. Everything else is an order no client can act on any more: invalid is terminal, and a pending/ready/processing order past its own expires is refused on read by every handler that loads one.

Scoped to one profile because order.retention_days is a per-profile key: two endpoints in one process may reasonably keep their history for different lengths of time.

Source

pub async fn count_by_account( account_id: Uuid, 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.

Source

pub async fn delete( id: &str, database: &Database, ) -> Result<GuardedDelete, Error>

Hard-deletes the order row — cascading, via ON DELETE CASCADE, to its authorizations and challenges — unless it holds a [live_certificate!], which it refuses.

The guard is part of the DELETE itself rather than a read before it, so no issuance can land between the check and the delete. Only when nothing was deleted does a second read tell a refusal from a missing row.

Source

pub async fn count_live_certificates( order_id: Uuid, database: &Database, ) -> Result<u64, Error>

How many live certificates ([live_certificate!]) this order holds: 0 or 1. What the order card reads to disable its delete button.

Source

pub async fn finalize( &mut self, chain: String, cert_serial: String, cert_pubkey: Vec<u8>, cert_not_after: Option<i64>, database: &Database, ) -> Result<(), Error>

Records a successful issuance: stores the PEM chain plus the leaf’s cert_serial (hex), cert_pubkey (DER SPKI) and cert_not_after — all three populated by the caller from that same chain via acme_proxy_core::cert::cert_serial_and_spki and acme_proxy_core::cert::cert_validity, 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.

cert_not_after is Option where the other two are not, and the asymmetry is deliberate: the serial and the public key are what make a certificate revocable, so a chain they cannot be read from is a failed issuance, while the expiry is housekeeping for the digest and a leaf this server cannot read the validity of must still be recorded as issued. Callers pass None rather than refusing — the rule LocalCa::revoke already follows when it records a revoked leaf’s expiry, and the sweep will try the chain again later.

Source

pub async fn find_expiring( profile: Option<&str>, before: i64, limit: i64, offset: i64, database: &Database, ) -> Result<(Vec<Order>, i64), Error>

The certificates expiring at or before before, soonest first, with the unpaged total beside the page — the digest’s whole query ([notify.expiry], notify::expiry) and the admin surfaces’ (GET /api/expiring, /ui/expiring, order list --expiring-in).

Three predicates, each carrying its own reason. certificate IS NOT NULL because an order that never issued has nothing to expire; revoked_at IS NULL because a withdrawn certificate is not something to go and renew; and cert_not_after >= 0 because a negative value is the sweep’s sentinel for a chain it could not parse, which is a row to leave alone rather than to report as expiring in 1970. They are exactly the partial index’s own predicate.

profile is an Option because the panel lists every endpoint by default, like every other admin listing, where the digest asks one profile at a time. The two forms cost different things, and the difference is the index’s column order:

  • Some is SEARCH … USING INDEX idx_orders_cert_not_after (profile=? AND cert_not_after>? AND cert_not_after<?) — a range seek on both columns, and byte for byte the plan the digest’s original query got.
  • None is SCAN … USING INDEX idx_orders_cert_not_after: the partial predicate still matches, so the index is still what is read, but with no leading-column equality there is nothing to seek to, and the index is ordered by cert_not_after only within a profile — so the ordering below falls to a temp b-tree over the whole result rather than over its last term alone.

That is the price of the unscoped view, and it is stated here rather than left to be rediscovered from a query plan. A profile-less index on cert_not_after would buy it back, and is not worth a second index on a table this one already covers until a deployment says otherwise.

The total is counted rather than derived from the page, so “…and N more” can be honest without loading a tail nobody will read. id breaks the ordering tie for Order::search’s reason: two certificates can share a whole-second expiry, and a stable order is what stops one of them being dropped between the page and the count.

Source

pub async fn find_unstamped( profile: &str, limit: i64, database: &Database, ) -> Result<Vec<(Uuid, String)>, Error>

The issued orders on profile whose cert_not_after has never been derived — rows finalized before the column existed. At most limit per call, since this parses an X.509 chain per row and the digest that calls it is not the only thing the runner has to do.

Returns (id, chain) pairs rather than whole orders: the caller wants the PEM and nothing else, and a digest running against a long-lived deployment would otherwise inflate every row it is about to discard.

Source

pub async fn set_cert_not_after( id: Uuid, cert_not_after: i64, database: &Database, ) -> Result<(), Error>

Writes a cert_not_after derived after the fact by the sweep.

Separate from Order::finalize because it is a backfill and not an issuance: it touches one column, never status, and it is the one caller that legitimately writes the negative sentinel — a chain that will not parse has to be recorded as unparsable, or every pass parses it again for the life of the deployment.

Source

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.

Source

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.

Source

pub async fn revoke( &mut self, reason: Option<i64>, database: &Database, ) -> Result<bool, 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.

Source

pub async fn set_revoked<'e>( id: Uuid, reason: Option<i64>, revoked_at: i64, executor: impl Into<Exec<'e>>, ) -> Result<bool, Error>

The revocation stamp as a bare statement, over any executor.

Split from Order::revoke so a revocation recorded without a signer round trip (acme::revoke’s ledger path) can write the order and the revocations row in one transaction. The in-memory sync is the caller’s, after the commit.

Guarded on the order not being revoked, and reports whether it wrote. A revocation is recorded once: the revocations ledger keeps the first reason (ON CONFLICT DO NOTHING), so an unguarded stamp here would leave the order naming a reason and a time the CRL does not, and a second caller writing a second audit row and a second notification for one withdrawal of trust.

Source

pub async fn set_invalid<'e>( id: Uuid, error: &Value, executor: impl Into<Exec<'e>>, ) -> Result<bool, Error>

The invalid transition as a bare statement, over any executor, returning whether it happened.

Split from Order::mark_invalid so a validation verdict can compose the challenge, authorization and order transitions into one transaction. The in-memory sync stays in mark_invalid, since it must not happen until the transaction has committed.

Guarded on the order not being decided. Every caller is a queued job that read the order earlier, and a valid order holds a live certificate: writing invalid over it would let Order::cleanup delete the only row that can revoke that certificate. An invalid order keeps the first error it was given.

Source

pub async fn set_ready<'e>( id: Uuid, executor: impl Into<Exec<'e>>, ) -> Result<bool, Error>

The ready transition as a bare statement, guarded on pending; see Order::set_invalid.

Source

pub async fn set_pending<'e>( id: Uuid, executor: impl Into<Exec<'e>>, ) -> Result<bool, Error>

The pending transition as a bare statement, guarded on ready; see Order::set_invalid.

The only backwards transition in the order state machine, taken when an authorization of a ready order stops being valid — in practice, a client deactivating one (RFC 8555 §7.5.2). §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, or finalize would still accept it. §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.

Source

pub async fn mark_invalid( &mut self, error: Value, database: &Database, ) -> Result<bool, Error>

Records a failed issuance: stores the error problem document, moves the order to the terminal invalid state, and keeps self in sync (like Order::finalize). Used when the signer fails internally; a badCSR leaves the order ready and retryable instead. false when the order was already decided; see Order::set_invalid.

Source

pub async fn mark_ready(&mut self, database: &Database) -> Result<bool, 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); false when it was not pending.

Source

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. finalize used to read the order, check it was ready, sign, and write — 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::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 the same guard. Now that issuance is queued, the claim is also what keys the signer_issue job: both land in one transaction.

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.

Source

pub async fn claim_for_finalize_on<'e>( &mut self, executor: impl Into<Exec<'e>>, ) -> Result<bool, Error>

claim_for_finalize on a connection of the caller’s — finalize takes the claim and queues the issuance that settles it in one transaction, so an order is never processing with nothing coming for it. self is updated as soon as the statement succeeds; a caller whose transaction then fails discards it.

Source

pub fn to_json(&self, base_url: &str, authz_ids: &[Uuid]) -> 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§

Source§

impl Debug for Order

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

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<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more