///|
/// A signed request before its signature is attached.
pub(all) struct PreparedRequest {
verb : String
url : String
draft : JwsDraft
accept : String
} derive(Eq, Debug)
///|
/// Transport-neutral HTTP request. Native, JavaScript and test transports only
/// need to send this value and return status, headers and bytes.
pub(all) struct HttpRequest {
verb : String
url : String
headers : Array[Header]
body : String
} derive(Eq, Debug)
///|
pub fn PreparedRequest::finish(
self : PreparedRequest,
signature : BytesView,
) -> HttpRequest raise AcmeError {
{
verb: self.verb,
url: self.url,
headers: [
{ name: "content-type", value: "application/jose+json", },
{ name: "accept", value: self.accept, },
{ name: "user-agent", value: "moonacme/0.1", },
],
body: self.draft.finish(signature),
}
}
///|
/// Protocol session state. The directory, account identity and replay nonces
/// live together so a request cannot accidentally use a nonce twice or send a
/// KID request before account creation.
pub struct AcmeSession {
directory : Directory
algorithm : String
public_jwk : String
mut kid : String?
nonces : NoncePool
}
///|
pub fn AcmeSession::new(
directory~ : Directory,
algorithm~ : String,
public_jwk~ : String,
) -> AcmeSession raise AcmeError {
match parse_json(public_jwk, "session.jwk") {
Object(_) => ()
_ => raise malformed("session.jwk", "public JWK must be a JSON object")
}
if algorithm.length() == 0 {
raise malformed("session", "empty signing algorithm")
}
{ directory, algorithm, public_jwk, kid: None, nonces: NoncePool::new(), }
}
///|
pub fn AcmeSession::account_url(self : AcmeSession) -> String? {
self.kid
}
///|
pub fn AcmeSession::bind_account(
self : AcmeSession,
kid : String,
) -> Unit raise AcmeError {
if kid.length() == 0 {
raise malformed("account", "empty account URL")
}
self.kid = Some(kid)
}
///|
pub fn AcmeSession::offer_nonce(self : AcmeSession, nonce : String) -> Bool {
self.nonces.offer(nonce)
}
///|
pub fn AcmeSession::observe_headers(
self : AcmeSession,
headers : Array[Header],
) -> Bool {
match replay_nonce(headers) {
Some(nonce) => self.offer_nonce(nonce)
None => false
}
}
///|
fn AcmeSession::binding(self : AcmeSession) -> AccountBinding raise AcmeError {
match self.kid {
Some(kid) => AccountBinding::KeyId(kid)
None =>
raise AcmeError::InvalidState(
expected="registered account URL",
actual="unregistered account",
)
}
}
///|
fn AcmeSession::prepare(
self : AcmeSession,
url : String,
payload : String,
binding : AccountBinding,
accept : String,
) -> PreparedRequest raise AcmeError {
let nonce = match self.nonces.take() {
Some(value) => value
None => raise AcmeError::MissingNonce
}
{
verb: "POST",
url,
draft: JwsDraft::new(
algorithm=self.algorithm,
nonce~,
url~,
payload~,
binding~,
),
accept,
}
}
///|
pub fn AcmeSession::prepare_new_account(
self : AcmeSession,
contacts : Array[String],
terms_agreed : Bool,
) -> PreparedRequest raise AcmeError {
self.prepare(
self.directory.new_account,
encode_new_account(contacts, terms_agreed),
AccountBinding::PublicJwk(self.public_jwk),
"application/json",
)
}
///|
pub fn AcmeSession::prepare_new_order(
self : AcmeSession,
identifiers : Array[Identifier],
) -> PreparedRequest raise AcmeError {
if identifiers.length() == 0 {
raise malformed("new-order", "at least one identifier is required")
}
self.prepare(
self.directory.new_order,
encode_new_order(identifiers),
self.binding(),
"application/json",
)
}
///|
pub fn AcmeSession::prepare_post_as_get(
self : AcmeSession,
url : String,
accept? : String = "application/json",
) -> PreparedRequest raise AcmeError {
self.prepare(url, "", self.binding(), accept)
}
///|
pub fn AcmeSession::prepare_challenge_ack(
self : AcmeSession,
challenge_url : String,
) -> PreparedRequest raise AcmeError {
self.prepare(challenge_url, "{}", self.binding(), "application/json")
}
///|
pub fn AcmeSession::prepare_finalize(
self : AcmeSession,
finalize_url : String,
csr_der : BytesView,
) -> PreparedRequest raise AcmeError {
if csr_der.length() == 0 {
raise malformed("finalize", "empty PKCS#10 CSR")
}
let payload = @json.dumps(
Json::object({ "csr": Json::string(base64url(csr_der)) }),
sort=true,
)
self.prepare(finalize_url, payload, self.binding(), "application/json")
}
///|
pub fn AcmeSession::prepare_download(
self : AcmeSession,
certificate_url : String,
) -> PreparedRequest raise AcmeError {
self.prepare_post_as_get(
certificate_url,
accept="application/pem-certificate-chain",
)
}
///|
pub fn AcmeSession::prepare_revoke(
self : AcmeSession,
certificate_der : BytesView,
) -> PreparedRequest raise AcmeError {
if certificate_der.length() == 0 {
raise malformed("revoke", "empty certificate")
}
let payload = @json.dumps(
Json::object({ "certificate": Json::string(base64url(certificate_der)) }),
sort=true,
)
self.prepare(
self.directory.revoke_cert,
payload,
self.binding(),
"application/json",
)
}