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 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 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>

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 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.

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<bool, Error>

Hard-deletes the order row — cascading, via ON DELETE CASCADE, to its authorizations and challenges. Returns whether a row existed to delete.

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 crate::cert::cert_serial_and_spki and crate::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], crate::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<(), 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 mark_invalid( &mut self, error: Value, database: &Database, ) -> Result<(), Error>

Source

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).

Source

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.

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. 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.

Source

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.

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> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. 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, <T as TryFrom<U>>::Error>

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