coapcore/seccfg.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
//! Descriptions of ACE Authorization Servers (AS) and other trust anchors, as viewed from the
//! Resource Server (RS) which coapcore runs on.
use defmt_or_log::{debug, error, trace};
use crate::ace::HeaderMap;
use crate::error::{CredentialError, CredentialErrorDetail};
pub const MAX_AUD_SIZE: usize = 8;
/// Error type of [`ServerSecurityConfig::render_not_allowed`].
///
/// This represents a failure to express the Request Creation Hints of ACE in a message. Unlike
/// most CoAP rendering errors, this can not just fall back to rendering that produces an Internal
/// Server Error, as that would be misunderstood by the client to mean that the requested operation
/// was being performed and failed at runtime (whereas with this error, the requested operation was
/// not performed). Therefore, no error details can be communicated to the client reliably.
///
/// Implementers are encouraged to log an error when returning this.
pub struct NotAllowedRenderingFailed;
/// A single or collection of authorization servers that a handler trusts to create ACE tokens.
pub trait ServerSecurityConfig: crate::Sealed {
/// True if the type will at any time need to process tokens at /authz-info
///
/// This is used by the handler implementation to shortcut through some message processing
/// paths.
const PARSES_TOKENS: bool;
/// The way scopes issued with this system as audience by this AS are expressed here.
type Scope: crate::scope::Scope;
/// Unprotects a symmetriclly encrypted token and processes the contained [CWT Claims
/// Set][crate::ace::CwtClaimsSet] into a [`Self::Scope`] and returns the claims.
///
/// The steps are performed together rather than in separate functions because it is yet
/// unclear how data would precisely be carried around. (Previous iterations of this API had a
/// `ScopeGenerator` associated type that would carry such data, but that did not scale well to
/// different kinds of tokens).
///
/// As part of such a dissection it would be preferable to return a decryption key and let the
/// `ace` module do the decryption, but the key is not dyn safe, and
/// [`aead::AeadInPlace`](https://docs.rs/aead/latest/aead/trait.AeadInPlace.html) can not be
/// enum'd around different potential key types because the associated types are fixed length.
/// (Returning a key in some COSE crypto abstraction may work better).
///
/// Note that the full AAD (COSE's AAD including the external AAD) is built by the caller; the
/// headers are only passed in to enable the AS to select the right key.
///
/// The buffer is given as heapless buffer rather than an an
/// [`aead::Buffer`](https://docs.rs/aead/latest/aead/trait.Buffer.html) because the latter is
/// not on the latest heaples version in its released version.
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
#[expect(
rustdoc::private_intra_doc_links,
reason = "Method is sealed by private types"
)]
// The method is already sealed by the use of a HeaderMap and CwtClaimsSet, but that may become
// more public over time, and that should not impct this method's publicness.
fn decrypt_symmetric_token<'buf>(
&self,
headers: &HeaderMap,
aad: &[u8],
ciphertext_buffer: &'buf mut [u8],
_: crate::PrivateMethod,
) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'buf>), CredentialError> {
Err(CredentialErrorDetail::KeyNotPresent.into())
}
/// Verify the signature on a symmetrically encrypted token
///
/// `signed_payload` is the payload part of the signed CWT; while it is part of `signed_data` and
/// can be recovered from it, `signed_data` currently typically resides in a copied buffer
/// created for signature verification, and signed_payload is around inside the caller for
/// longer. As common with signed data, it should only be parsed once the signature has been
/// verified.
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn verify_asymmetric_token<'b>(
&self,
headers: &HeaderMap,
signed_data: &[u8],
signature: &[u8],
signed_payload: &'b [u8],
_: crate::PrivateMethod,
) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'b>), CredentialError> {
Err(CredentialErrorDetail::KeyNotPresent.into())
}
fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
None
}
/// Expands an EDHOC `ID_CRED_x` into a parsed `CRED_x` along with the associated
/// authorizations.
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn expand_id_cred_x(
&self,
id_cred_x: lakers::IdCred,
) -> Option<(lakers::Credential, Self::Scope)> {
None
}
/// Generates the scope representing unauthenticated access.
fn nosec_authorization(&self) -> Option<Self::Scope> {
None
}
/// Render the "not allowed" message in this scenario.
///
/// The default (or any error) renderer produces a generic 4.01 Unauthorized in the handler;
/// specifics can be useful in ACE scenarios to return a Request Creation Hint.
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn render_not_allowed<M: coap_message::MutableWritableMessage>(
&self,
message: &mut M,
) -> Result<(), NotAllowedRenderingFailed> {
Err(NotAllowedRenderingFailed)
}
}
/// The default empty configuration that denies all access.
pub struct DenyAll;
impl crate::Sealed for DenyAll {}
impl ServerSecurityConfig for DenyAll {
const PARSES_TOKENS: bool = false;
type Scope = core::convert::Infallible;
}
/// An SSC representing unconditionally allowed access, including unencrypted.
pub struct AllowAll;
impl crate::Sealed for AllowAll {}
impl ServerSecurityConfig for AllowAll {
const PARSES_TOKENS: bool = false;
type Scope = crate::scope::AllowAll;
fn nosec_authorization(&self) -> Option<Self::Scope> {
Some(crate::scope::AllowAll)
}
}
/// An implementation of [`ServerSecurityConfig`] that can be extended using builder methods.
///
/// This is very much in flux, and will need further exploration as to inhowmuch this can be
/// type-composed from components.
pub struct ConfigBuilder {
/// Symmetric used when tokens are symmetrically encrypted with AES-CCM-16-128-256
as_key_31: Option<[u8; 32]>,
/// Asymmetric key used when tokens are signed with ES256
///
/// Alogn with the key, this also holds the audience value of this RS (as signed tokens only
/// make sense when the same signing key is used with multiple recipients).
as_key_neg7: Option<([u8; 32], [u8; 32], heapless::String<MAX_AUD_SIZE>)>,
unauthenticated_scope: Option<crate::scope::UnionScope>,
own_edhoc_credential: Option<(lakers::Credential, lakers::BytesP256ElemLen)>,
known_edhoc_clients: Option<(lakers::Credential, crate::scope::UnionScope)>,
request_creation_hints: &'static [u8],
}
impl crate::Sealed for ConfigBuilder {}
impl ServerSecurityConfig for ConfigBuilder {
// We can't know at build time, assume yes
const PARSES_TOKENS: bool = true;
type Scope = crate::scope::UnionScope;
fn decrypt_symmetric_token<'buf>(
&self,
headers: &HeaderMap,
aad: &[u8],
ciphertext_buffer: &'buf mut [u8],
_: crate::PrivateMethod,
) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'buf>), CredentialError> {
use ccm::aead::AeadInPlace;
use ccm::KeyInit;
let key = self.as_key_31.ok_or_else(|| {
error!("Symmetrically encrypted token was sent, but no symmetric key is configured.");
CredentialErrorDetail::KeyNotPresent
})?;
// FIXME: should be something Aes256Ccm::TagLength
const TAG_SIZE: usize = 16;
const NONCE_SIZE: usize = 13;
pub type Aes256Ccm = ccm::Ccm<aes::Aes256, ccm::consts::U16, ccm::consts::U13>;
let cipher = Aes256Ccm::new((&key).into());
let nonce: &[u8; NONCE_SIZE] = headers
.iv
.ok_or_else(|| {
error!("IV missing from token.");
CredentialErrorDetail::InconsistentDetails
})?
.try_into()
.map_err(|_| {
error!("Token's IV length mismatches algorithm.");
CredentialErrorDetail::InconsistentDetails
})?;
let ciphertext_len = ciphertext_buffer
.len()
.checked_sub(TAG_SIZE)
.ok_or_else(|| {
error!("Token's ciphertext too short for the algorithm's tag.");
CredentialErrorDetail::InconsistentDetails
})?;
let (ciphertext, tag) = ciphertext_buffer.split_at_mut(ciphertext_len);
cipher
.decrypt_in_place_detached(nonce.into(), aad, ciphertext, ccm::Tag::from_slice(tag))
.map_err(|_| {
error!("Token decryption failed.");
CredentialErrorDetail::VerifyFailed
})?;
let claims: crate::ace::CwtClaimsSet = minicbor::decode(ciphertext)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;
let scope = crate::scope::AifValue::parse(claims.scope)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;
Ok((scope.into(), claims))
}
fn verify_asymmetric_token<'b>(
&self,
headers: &HeaderMap,
signed_data: &[u8],
signature: &[u8],
signed_payload: &'b [u8],
_: crate::PrivateMethod,
) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'b>), CredentialError> {
if headers.alg != Some(-7) {
// ES256
return Err(CredentialErrorDetail::UnsupportedAlgorithm.into());
}
let Some((x, y, rs_audience)) = self.as_key_neg7.as_ref() else {
return Err(CredentialErrorDetail::KeyNotPresent.into());
};
use p256::ecdsa::{signature::Verifier, VerifyingKey};
let as_key = VerifyingKey::from_encoded_point(
&p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false),
)
.map_err(|_| CredentialErrorDetail::InconsistentDetails)?;
let signature = p256::ecdsa::Signature::from_slice(signature)
.map_err(|_| CredentialErrorDetail::InconsistentDetails)?;
as_key
.verify(signed_data, &signature)
.map_err(|_| CredentialErrorDetail::VerifyFailed)?;
let claims: crate::ace::CwtClaimsSet = minicbor::decode(signed_payload)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;
if claims.aud != Some(rs_audience) {
// FIXME describe better? "Verified but we're not the audience?"
return Err(CredentialErrorDetail::VerifyFailed.into());
}
let scope = crate::scope::AifValue::parse(claims.scope)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;
Ok((scope.into(), claims))
}
fn nosec_authorization(&self) -> Option<Self::Scope> {
self.unauthenticated_scope.clone()
}
fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
self.own_edhoc_credential
}
fn expand_id_cred_x(
&self,
id_cred_x: lakers::IdCred,
) -> Option<(lakers::Credential, Self::Scope)> {
trace!(
"Evaluating peer's credential {=[u8]:02x}", // :02x could be :cbor
id_cred_x.as_full_value()
);
#[expect(
clippy::single_element_loop,
reason = "Expected to be extended to actual loop soon"
)]
for (credential, scope) in &[self.known_edhoc_clients.as_ref()?] {
trace!("Comparing to {=[u8]:02x}", credential.bytes.as_slice()); // :02x could be :cbor
if id_cred_x.reference_only() {
// ad Ok: If our credential has no KID, it can't be recognized in this branch
if credential.by_kid() == Ok(id_cred_x) {
debug!("Peer indicated use of the one preconfigured key by KID.");
#[expect(
clippy::clone_on_copy,
reason = "Lakers items are overly copy happy"
)]
return Some((credential.clone(), scope.clone()));
}
} else {
// ad Ok: This is always the case for CCSs, but inapplicable eg. for PSKs.
if credential.by_value() == Ok(id_cred_x) {
debug!("Peer indicated use of the one preconfigured credential by value.");
#[expect(
clippy::clone_on_copy,
reason = "Lakers items are overly copy happy"
)]
return Some((credential.clone(), scope.clone()));
}
}
}
if let Some(small_scope) = self.nosec_authorization() {
trace!("Unauthenticated clients are generally accepted, evaluating credential.");
if let Some(credential_by_value) = id_cred_x.get_ccs() {
debug!("The unauthorized client provided a usable credential by value.");
#[expect(clippy::clone_on_copy, reason = "Lakers items are overly copy happy")]
return Some((credential_by_value.clone(), small_scope.clone()));
}
}
None
}
fn render_not_allowed<M: coap_message::MutableWritableMessage>(
&self,
message: &mut M,
) -> Result<(), NotAllowedRenderingFailed> {
use coap_message::Code;
message.set_code(M::Code::new(coap_numbers::code::UNAUTHORIZED).map_err(|_| {
error!("CoAP stack can not represent Unauthorized responses.");
NotAllowedRenderingFailed
})?);
message
.set_payload(self.request_creation_hints)
.map_err(|_| {
error!("Request creation hints do not fit in error message.");
NotAllowedRenderingFailed
})?;
Ok(())
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
ConfigBuilder::new()
}
}
impl ConfigBuilder {
/// Creates an empty server security configuration.
///
/// Without any additional building steps, this is equivalent to [`DenyAll`].
pub fn new() -> Self {
Self {
as_key_31: None,
as_key_neg7: None,
unauthenticated_scope: None,
known_edhoc_clients: None,
own_edhoc_credential: None,
request_creation_hints: &[],
}
}
/// Sets a single Authorization Server recognized by a shared `AES-16-128-256` (COSE algorithm
/// 31) key.
///
/// Scopes are accepted as given by the AS using the AIF REST model as understood by
/// [`crate::scope::AifValue`].
///
/// # Caveats and evolution
///
/// Currently, this type just supports a single AS; it should therefore only be called once,
/// and the latest value overwrites any earlier. Building these in type state (as `[(&as_key);
/// { N+1 }]` (once that is possible) or `(&as_key1, (&as_key2, ()))` will make sense on the
/// long run, but is not implemented yet.
///
/// Depending on whether the keys are already referenced in a long-lived location, when
/// implementing that, it can also make sense to allow using any `AsRef<[u8; 32]>` types at
/// that point.
///
/// Currently, keys are taken as byte sequence. With the expected flexibilization of crypto
/// backends, this may later allow a more generic type that reflects secure element key slots.
pub fn with_aif_symmetric_as_aesccm256(self, key: [u8; 32]) -> Self {
Self {
as_key_31: Some(key),
..self
}
}
/// Sets a single Authorization Server recignized by its `ES256` (COSE algorithm -7) signing
/// key.
///
/// An audience identifier is taken along with the key; signed tokens are only accepted if they
/// have that audience.
///
/// Scopes are accepted as given by the AS using the AIF REST model as understood by
/// [`crate::scope::AifValue`].
///
/// # Caveats and evolution
///
/// Same from [`Self::with_aif_symmetric_as_aesccm256`] apply, minus the considerations for
/// secure key storage.
pub fn with_aif_asymmetric_es256(
self,
x: [u8; 32],
y: [u8; 32],
audience: heapless::String<MAX_AUD_SIZE>,
) -> Self {
Self {
as_key_neg7: Some((x, y, audience)),
..self
}
}
/// Allow use of the server within the limits of the given scope by EDHOC clients provided they
/// present the given credential.
///
/// # Caveats and evolution
///
/// Currently, this type just supports a single credential; it should therefore only be called
/// once, and the latest value overwrites any earlier. (See
/// [`Self::with_aif_symmetric_as_aesccm256`] for plans).
pub fn with_known_edhoc_credential(
self,
credential: lakers::Credential,
scope: crate::scope::UnionScope,
) -> Self {
Self {
known_edhoc_clients: Some((credential, scope)),
..self
}
}
/// Configures an EDHOC credential and private key to be presented by this server.
///
/// # Panics
///
/// When debug assertions are enabled, this panics if an own credential has already been
/// configured.
pub fn with_own_edhoc_credential(
self,
credential: lakers::Credential,
key: lakers::BytesP256ElemLen,
) -> Self {
debug_assert!(
self.own_edhoc_credential.is_none(),
"Overwriting previously configured own credential scope"
);
Self {
own_edhoc_credential: Some((credential, key)),
..self
}
}
/// Allow use of the server by unauthenticated clients using the given scope.
///
/// # Panics
///
/// When debug assertions are enabled, this panics if an unauthenticated scope has already been
/// configured.
pub fn allow_unauthenticated(self, scope: crate::scope::UnionScope) -> Self {
debug_assert!(
self.unauthenticated_scope.is_none(),
"Overwriting previously configured unauthenticated scope"
);
Self {
unauthenticated_scope: Some(scope),
..self
}
}
/// Sets the payload of the "Unauthorized" response.
///
/// # Panics
///
/// When debug assertions are enabled, this panics if an unauthenticated scope has already been
/// configured.
pub fn with_request_creation_hints(self, request_creation_hints: &'static [u8]) -> Self {
debug_assert!(
self.request_creation_hints.is_empty(),
"Overwriting previously configured unauthenticated scope"
);
Self {
request_creation_hints,
..self
}
}
}