Skip to main content

Order

Struct Order 

Source
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

  • 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).
  • 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: String§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: 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

Source

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.

Source

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

Source

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.

Source

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.

Source

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.

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.

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.

Source

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.

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

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: &[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§

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

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