add external account binding

Functionality was added as a additional setter function, which hopefully
prevents any breakages. Since a placeholder Option an the AccountData
was already present, but has never been used, replacing the field with
an Option of a fully defined type should also be minimally intrusive.

Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
This commit is contained in:
Folke Gleumes 2023-11-14 15:14:00 +01:00 committed by Wolfgang Bumiller
parent 7e6aa2733a
commit 88f7e190ec
4 changed files with 99 additions and 6 deletions

View File

@ -11,8 +11,9 @@ use serde_json::Value;
use crate::authorization::{Authorization, GetAuthorization};
use crate::b64u;
use crate::directory::Directory;
use crate::eab::ExternalAccountBinding;
use crate::jws::Jws;
use crate::key::PublicKey;
use crate::key::{Jwk, PublicKey};
use crate::order::{NewOrder, Order, OrderData};
use crate::request::Request;
use crate::Error;
@ -336,10 +337,9 @@ pub struct AccountData {
#[serde(skip_serializing_if = "Option::is_none")]
pub terms_of_service_agreed: Option<bool>,
/// External account information. This is currently not directly supported in any way and only
/// stored to completeness.
/// External account information.
#[serde(skip_serializing_if = "Option::is_none")]
pub external_account_binding: Option<Value>,
pub external_account_binding: Option<ExternalAccountBinding>,
/// This is only used by the client when querying an account.
#[serde(default = "default_true", skip_serializing_if = "is_false")]
@ -375,6 +375,7 @@ pub struct AccountCreator {
contact: Vec<String>,
terms_of_service_agreed: bool,
key: Option<PKey<Private>>,
eab_credentials: Option<(String, PKey<Private>)>,
}
impl AccountCreator {
@ -402,6 +403,13 @@ impl AccountCreator {
self
}
/// Set the EAB credentials for the account registration
pub fn set_eab_credentials(mut self, kid: String, hmac_key: String) -> Result<Self, Error> {
let hmac_key = PKey::hmac(&base64::decode(hmac_key)?)?;
self.eab_credentials = Some((kid, hmac_key));
Ok(self)
}
/// Generate a new RSA key of the specified key size.
pub fn generate_rsa_key(self, bits: u32) -> Result<Self, Error> {
let key = openssl::rsa::Rsa::generate(bits)?;
@ -431,6 +439,15 @@ impl AccountCreator {
/// [`response`](AccountCreator::response()) will render the account unusable!
pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> {
let key = self.key.as_deref().ok_or(Error::MissingKey)?;
let url = directory.new_account_url();
let external_account_binding = self
.eab_credentials
.as_ref()
.map(|cred| {
ExternalAccountBinding::new(&cred.0, &cred.1, Jwk::try_from(key)?, url.to_string())
})
.transpose()?;
let data = AccountData {
orders: None,
@ -441,12 +458,11 @@ impl AccountCreator {
} else {
None
},
external_account_binding: None,
external_account_binding,
only_return_existing: false,
extra: HashMap::new(),
};
let url = directory.new_account_url();
let body = serde_json::to_string(&Jws::new(
key,
None,

66
src/eab.rs Normal file
View File

@ -0,0 +1,66 @@
use openssl::hash::MessageDigest;
use openssl::pkey::{HasPrivate, PKeyRef};
use openssl::sign::Signer;
use serde::{Deserialize, Serialize};
use crate::key::Jwk;
use crate::{b64u, Error};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Protected {
alg: &'static str,
url: String,
kid: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ExternalAccountBinding {
protected: String,
payload: String,
signature: String,
}
impl ExternalAccountBinding {
pub fn new<P>(
eab_kid: &str,
eab_hmac_key: &PKeyRef<P>,
jwk: Jwk,
url: String,
) -> Result<Self, Error>
where
P: HasPrivate,
{
let protected = Protected {
alg: "HS256",
kid: eab_kid.to_string(),
url,
};
let payload = b64u::encode(serde_json::to_string(&jwk)?.as_bytes());
let protected_data = b64u::encode(serde_json::to_string(&protected)?.as_bytes());
let signature = {
let protected = protected_data.as_bytes();
let payload = payload.as_bytes();
Self::sign_hmac(eab_hmac_key, protected, payload)?
};
let signature = b64u::encode(&signature);
Ok(ExternalAccountBinding {
protected: protected_data,
payload,
signature,
})
}
fn sign_hmac<P>(key: &PKeyRef<P>, protected: &[u8], payload: &[u8]) -> Result<Vec<u8>, Error>
where
P: HasPrivate,
{
let mut signer = Signer::new(MessageDigest::sha256(), key)?;
signer.update(protected)?;
signer.update(b".")?;
signer.update(payload)?;
Ok(signer.sign_to_vec()?)
}
}

View File

@ -59,6 +59,9 @@ pub enum Error {
/// An otherwise uncaught serde error happened.
Json(serde_json::Error),
/// Failed to parse
BadBase64(base64::DecodeError),
/// Can be used by the user for textual error messages without having to downcast to regular
/// acme errors.
Custom(String),
@ -121,6 +124,7 @@ impl fmt::Display for Error {
Error::HttpClient(err) => fmt::Display::fmt(err, f),
Error::Client(err) => fmt::Display::fmt(err, f),
Error::Csr(err) => fmt::Display::fmt(err, f),
Error::BadBase64(err) => fmt::Display::fmt(err, f),
}
}
}
@ -142,3 +146,9 @@ impl From<crate::request::ErrorResponse> for Error {
Error::Api(e)
}
}
impl From<base64::DecodeError> for Error {
fn from(e: base64::DecodeError) -> Self {
Error::BadBase64(e)
}
}

View File

@ -14,6 +14,7 @@
#![deny(missing_docs)]
mod b64u;
mod eab;
mod json;
mod jws;
mod key;