Repository: openwsn-berkeley/lakers (language: python)

Issue:
Role struct constructors
Attempting to use edhoc-rs in a demo application, I found a few properties of the role constructors (eg. `EdhocInitiator`) odd:

* <del>The g_i / g_r is fed in explicitly -- from an application point of view I'd expect the library to either use the platform's random number generator (or, when that is not possible, to provide a random number callback that'll pull as many bytes as needed).</del> [edit: see <https://github.com/openwsn-berkeley/edhoc-rs/issues/99#issuecomment-1740520737>, these are the long-term keys]
* The constructors expect to be fed both sides' credentials -- the responder needs to know in advance which id_cred_i and cred_i the initiator will send, whereas I'd expect EDHOC operations to work with arbitrary initiators. It'd still be up to the application to provide the right CRED_R for an ID_CRED_R, but that could be a map in the simplest case (esp. when KIDs are used), or network based in more complex ones.
* <del>All those binary arguments are passed in hex encoded strings -- a conversion I'd prefer never to do on an embedded system.</del> [edit:fixed in <https://github.com/openwsn-berkeley/edhoc-rs/pull/101>]

I think I could provide patches for most of those, but these might be here for a reason. Could you explain why those are as they are, and whether those are boundary conditions set up by hacspec, or maybe just remnants of earlier development phases? If hacspec tightens things down here, do you have a roadmap for how to provide additional interfaces for constrained devices when hacspec limitations do not apply?


Discussion / hints:
Thanks for the feedback. See below.

> The g_i / g_r is fed in explicitly -- from an application point of view I'd expect the library to either use the platform's random number generator (or, when that is not possible, to provide a random number callback that'll pull as many bytes as needed).

Note that these are not the ephemeral keys. These are the public keys present in the corresponding CRED_I/CRED_R. For the moment, we do not parse the credentials for the sake of simplicity but rather use these values in the corresponding ECDH calculations.

> The constructors expect to be fed both sides' credentials -- the responder needs to know in advance which id_cred_i and cred_i the initiator will send, whereas I'd expect EDHOC operations to work with arbitrary initiators. It'd still be up to the application to provide the right CRED_R for an ID_CRED_R, but that could be a map in the simplest case (esp. when KIDs are used), or network based in more complex ones.

This is indeed a current limitation of the API. We would be happy to accept a PR on this.

> All those binary arguments are passed in hex encoded strings -- a conversion I'd prefer never to do on an embedded system.

True. I went through `lib/src/lib.rs` quickly and it seems byte strings would work equally well. I do no recall why the particular choice to use hex-encoded string was made. Again, happy to accept a PR on this.

Note that with the evolution of the hacspec framework, we can now use it on the vanilla Rust branch of the code. That means that we will be getting rid of the hacspec version of the code quite soon, so if you are going to work on the code base, I would suggest doing so on `lib/src/lib.rs` and `lib/src/edhoc.rs`.
That's good to hear!

> Note that these are not the ephemeral keys.

Right, I mixed those up. (So then, providing the g_peer will be up to whatever provides the cred_peer for the provided id_cred_peer).

> would suggest doing so on lib/src/lib.rs and lib/src/edhoc.rs.

Those two have large `mod hacspec` / `mod rust` blocks; will these be unified, and is there a branch I can look at to see what of those survives? (In particular, will the `array!()` thingies still be around?)

For testing of upcoming PRs, is there anything I should do locally, or can I trust that CI will complain if I leave the hacspec parts behind somehow?
> Those two have large mod hacspec / mod rust blocks; will these be unified, and is there a branch I can look at to see what of those survives? (In particular, will the array!() thingies still be around?)

Note that only `lib/src/lib.rs` should have `rust` and `hacspec` blocks. Code in `lib/src/edhoc.rs` is vanilla Rust code. @geonnave is working on a branch where hacspec parts are removed and will let you know. The idea is to completely remove the "hacspec" blocks so `array!()` macros will go away!

>For testing of upcoming PRs, is there anything I should do locally, or can I trust that CI will complain if I leave the hacspec parts behind somehow?

The CI does all the necessary steps, but the hacspec checks will run only on the hacspec part of the code. So, for now, work on the Rust code and we can test later the hacspec aspects. (Please do not use early returns as these won't work when hacspec-checked).
Looking at this with some evenings into the library, and looking at all the data passed in at the constructor, it seems to me that the end game of a `EdhocResponder::new()` might look quite different from now, in that it would create its default state because
* r, cred_r and id_cred_r may be chosen as a result of processing message 1 (there's no draft on that yet, but if we ever did something like encrypted client hello, that's where it'd land)
* id_cred_i will be learned in message 3
* cred_i and g_i will be provided by the application after message 3 processing

This can all be expressed as extra arguments and return values in the right functions (and implementing it is one of my next steps). However, I'm wondering whether given the strict sequence (out-of-sequence calls make error go WrongState anyway), and given that the `.state` is taken and replaced anyway (also owing to hacspec's functional nature), may it not make sense to embrace typestate in the next iteration after that? (After learning some more of how this library works, it seems to me that the lib/src/lib.rs part will not be verified, so we'd be rather free in creating an idiomatic API there.)

So a mid-term proposal could look like

```rust
let a: EdhocResponder = Default::default();
let (a, ead1) = a.process_message_1(); // now a is of type EdhocResponderPhase1
let (a, m2) = a.prepare_message_2(...); // now a is of type EdhocResponderPhase2
```

and any error in sequence would be spotted by the type checker, instead of returning WrongState at runtime. There'd probably still be checks for the state (because the inner State object would stay the same, and we can't easily express its invariant to Rust), but some good const propagation could still make them go away completely.
Trying around, this is going to affect edhoc.rs more, because we'll have to split up `r_process_message_3` (and others if we want to decide the identity of R based on message 1's EAD). That is tricky because
1. it'll need to process the same message twice (OK, maybe not b/c decode_plaintext_3 returns owned data, but that may not be that way forever),
2. it's about processing processing encrypted data before they are authenticated (which is generally something to be very careful about towards the application), and
3. we're touching part of what makes verification happen.

However, the alternative is to decide CRED_I / G_I in a callback from within r_process_message_3, which I can't imagine makes anything any better, especially because that callback may block / may require async. (For example if looking up the credential takes time b/c it happens over a network and we have a cache miss).

Let's ignore 1 for the time being -- it may be a lot of data but it's bounded as long as we have limits on the EAD size we can process. I'd like to sketch a proposal first -- this would replace what was previously a `let (state, prk_out) = r_process_message_3(state, message_3, id_cred_i_expected, cred_i_expected, g_i)?` call:

```rust
let (state, mac_3, id_cred_i, ead_3_untrusted) = r_process_message_3a(state, message_3)?;
let (cred_i, g_i) = look_up_credentials(&id_cred_i); // may also use ead_3_untrusted
let (state, prk_out) = r_process_message_3b(state, mac_3, id_cred_i, ead_3_untrusted, cred_i, g_i)?;
// As that was successful, we may now also rename ead_3_untrusted to ead_3
```

Assembling the check from a mac_3 and the ead_3_untrusted is the alternative to passing in the message_3 once more -- none of which I'm really happy with, given that all things might go wrong if the user sends in the wrong mac_3 / ead_3_untrusted. The alternative is lugging them around with the state (which gets moved around anyway), but I'm not sure how well this would fly with hax:

```rust
let state_and_more = r_process_message_3a(state, message_3)?; // different type from `state`
let (cred_i, g_i) = look_up_credentials(state_and_more.id_cred_i()); // may also use accessor for ead_3_untrusted
let (state, prk_out) = r_process_message_3b(state_and_more, cred_i, g_i)?;
// ead_3 is now lost, but had we cloned it out, we could now trust it
// (If we were willing to carry it around, the latest state could also keep it; the compiler should
// throw it away anyway if unused if LTO is used)
```

I have a soft preference for approach 2 (which also moves the State even of the `edhoc` module into a typestate direction), but both should work, especially given that the lib module will be wrapping it anyway. So the larger question for me is: Which of those works for the verification?

(One way of addressing the question is: Where does the verifier even know what `r_process_message_3` is supposed to do? I only found two mentions in the code, in lib's `lib.rs` and `edhoc.rs`.)
>Looking at this with some evenings into the library, and looking at all the data passed in at the constructor, it seems to me that the end game of a EdhocResponder::new() might look quite different from now, in that it would create its default state because

> * r, cred_r and id_cred_r may be chosen as a result of processing message 1 (there's no draft on that yet, but if we ever did something like encrypted client hello, that's where it'd land)
> * id_cred_i will be learned in message 3
> * cred_i and g_i will be provided by the application after message 3 processing
> This can all be expressed as extra arguments and return values in the right functions (and implementing it is one of my next steps).

Agreed.

> However, I'm wondering whether given the strict sequence (out-of-sequence calls make error go WrongState anyway), and given that the .state is taken and replaced anyway (also owing to hacspec's functional nature), may it not make sense to embrace typestate in the next iteration after that? (After learning some more of how this library works, it seems to me that the lib/src/lib.rs part will not be verified, so we'd be rather free in creating an idiomatic API there.)

Indeed, we organized the library in this manner for exactly the reason you outline. We can and should provide an idiomatic API in `lib/src/lib.rs` that won't get hascpec-verified, but everything inside of `lib/src/edhoc.rs` goes under the hacspec scrutiny. Using typestate indeed seems to make sense, as long as it can all be contained in `lib/src/lib.rs`.

> So a mid-term proposal could look like

>  ```
> let a: EdhocResponder = Default::default();
> let (a, ead1) = a.process_message_1(); // now a is of type EdhocResponderPhase1
> let (a, m2) = a.prepare_message_2(...); // now a is of type EdhocResponderPhase2
> ```

This looks great!

> There'd probably still be checks for the state (because the inner State object would stay the same, and we can't easily express its invariant to Rust), but some good const propagation could still make them go away completely.

Just to make sure we are on the same page here, when you refer to the "inner" State object, do you mean the value of state passed to the hacspec-verified functions? Don't we still need those checks to verify the correct progress of hte state machine? e.g. how will typeset make sure that we do not process the same message twice if such a message was received over the wire?
> Don't we still need those checks to verify the correct progress of hte state machine?

They will, but may use results from the outside in const propagation. Ideally, we could inform the compiler that an invariant needs to be upheld -- say, `PublicStateAfterMessage2 { state_for_edhocrs: State }` might declare that this type has an invariant that `self.state_for_edhocrs == EdhocState::WaitMessage3`. We don't have that in Rust now, but tools emulating this may place the assert already at the accessor level (so by the time r_process_message_3 is called, the compiler knows that branch can't occur b/c of the earlier assert) or may even use assume versions (with due care: check at store time, `if state !=WaitMessage3 { unsafe { unsafe_unreachable() } }`) that moves all the checks to one point in time.
> Trying around, this is going to affect edhoc.rs more, because we'll have to split up `r_process_message_3` (and others if we want to decide the identity of R based on message 1's EAD). That is tricky because
> 
> 1. it'll need to process the same message twice (OK, maybe not b/c decode_plaintext_3 returns owned data, but that may not be that way forever),
> 2. it's about processing processing encrypted data before they are authenticated (which is generally something to be very careful about towards the application), and
> 3. we're touching part of what makes verification happen.
> 
> However, the alternative is to decide CRED_I / G_I in a callback from within r_process_message_3, which I can't imagine makes anything any better, especially because that callback may block / may require async. (For example if looking up the credential takes time b/c it happens over a network and we have a cache miss).
> 
> Let's ignore 1 for the time being -- it may be a lot of data but it's bounded as long as we have limits on the EAD size we can process. I'd like to sketch a proposal first -- this would replace what was previously a `let (state, prk_out) = r_process_message_3(state, message_3, id_cred_i_expected, cred_i_expected, g_i)?` call:
> 
> ```rust
> let (state, mac_3, id_cred_i, ead_3_untrusted) = r_process_message_3a(state, message_3)?;
> let (cred_i, g_i) = look_up_credentials(&id_cred_i); // may also use ead_3_untrusted
> let (state, prk_out) = r_process_message_3b(state, mac_3, id_cred_i, ead_3_untrusted, cred_i, g_i)?;
> // As that was successful, we may now also rename ead_3_untrusted to ead_3
> ```
> 
> Assembling the check from a mac_3 and the ead_3_untrusted is the alternative to passing in the message_3 once more -- none of which I'm really happy with, given that all things might go wrong if the user sends in the wrong mac_3 / ead_3_untrusted. The alternative is lugging them around with the state (which gets moved around anyway), but I'm not sure how well this would fly with hax:
> 
> ```rust
> let state_and_more = r_process_message_3a(state, message_3)?; // different type from `state`
> let (cred_i, g_i) = look_up_credentials(state_and_more.id_cred_i()); // may also use accessor for ead_3_untrusted
> let (state, prk_out) = r_process_message_3b(state_and_more, cred_i, g_i)?;
> // ead_3 is now lost, but had we cloned it out, we could now trust it
> // (If we were willing to carry it around, the latest state could also keep it; the compiler should
> // throw it away anyway if unused if LTO is used)
> ```
> 
> I have a soft preference for approach 2 (which also moves the State even of the `edhoc` module into a typestate direction), but both should work, especially given that the lib module will be wrapping it anyway. So the larger question for me is: Which of those works for the verification?
> 
> (One way of addressing the question is: Where does the verifier even know what `r_process_message_3` is supposed to do? I only found two mentions in the code, in lib's `lib.rs` and `edhoc.rs`.)

Before we discard the callback option, let me understand the pitfalls a bit better. I understand that retrieving `CRED_I` over the network may take time, but can we not just block until that is resolved? What would be an issue with that?

Other than that, on splitting the message 3 processing: I think that approach 1 is dangerous as it lets the application handle internal state of the protocol. I think it is out of the question.

As per approach 2, IIUC, you would essentially be adding `mac_3` and `ead_3` to state, and adding an additional state in the state machine to expect this (implemented with typestate). That seems fine as an approach to me, although I would ideally prefer to keep message_3 processing in one piece.

It would be great if @karthikbhargavan could chime in here.
> just block until that is resolved

A lot of embedded Rust environments don't block, but run on async instead. While it's certainly possible to have versions of the r_process_message_3 function around for blocking and async evaluation, I doubt that'd fly well with the verification, and I also don't think it's pretty (which is really an odd term to say that I think it's not good practice but can't express the arguments well yet).

> adding mac_3 and ead_3 to state

To understand better the impact of this I'd like to add to the open questions: Why is the state struct that's passed around between the phases of the protocol of the same type all the time? AFAICT, every function takes it only in one state and on exit it is in one state (unless there's an error), so why not typestate in the first place? (That'd make carrying mac_3 and ead_3 less hard).
> To understand better the impact of this I'd like to add to the open questions: Why is the state struct that's passed around between the phases of the protocol of the same type all the time? AFAICT, every function takes it only in one state and on exit it is in one state (unless there's an error), so why not typestate in the first place? (That'd make carrying mac_3 and ead_3 less hard).

There is no good explanation for this. We discussed previously splitting it but have not implemented it yet.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index c6576a9f..bc5641c8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ members = [
   "ead",
   "ead/lakers-ead-none",
   "ead/lakers-ead-zeroconf",
+  "ead/lakers-ead-authz",
   "crypto",
   "crypto/lakers-crypto-cc2538",
   "crypto/lakers-crypto-hacspec",
diff --git a/ead/lakers-ead-authz/Cargo.toml b/ead/lakers-ead-authz/Cargo.toml
new file mode 100644
index 00000000..c45a4fd4
--- /dev/null
+++ b/ead/lakers-ead-authz/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "lakers-ead-authz"
+version.workspace = true
+edition = "2021"
+authors = ["Geovane Fedrecheski <geonnave@gmail.com>"]
+license.workspace = true
+description = "EDHOC EAD zero touch authorization for network join (draf-lake-authz)"
+repository.workspace = true
+
+[dependencies]
+lakers-shared.workspace = true
+
+[dev-dependencies]
+lakers-crypto.workspace = true
+hexlit = "0.5.3"
diff --git a/ead/lakers-ead-authz/src/authenticator.rs b/ead/lakers-ead-authz/src/authenticator.rs
new file mode 100644
index 00000000..fe42d755
--- /dev/null
+++ b/ead/lakers-ead-authz/src/authenticator.rs
@@ -0,0 +1,213 @@
+use super::shared::*;
+use lakers_shared::{Crypto as CryptoTrait, *};
+
+#[derive(Debug, Default)]
+pub struct ZeroTouchAuthenticator;
+#[derive(Debug, Default)]
+pub struct ZeroTouchAuthenticatorWaitVoucherResp;
+
+impl ZeroTouchAuthenticator {
+    pub fn process_ead_1(
+        &self,
+        ead_1: &EADItem,
+        message_1: &EdhocMessageBuffer,
+    ) -> Result<
+        (
+            EdhocMessageBuffer,
+            EdhocMessageBuffer,
+            ZeroTouchAuthenticatorWaitVoucherResp,
+        ),
+        EDHOCError,
+    > {
+        let opaque_state: Option<EdhocMessageBuffer> = None; // TODO: receive as parameter
+
+        if ead_1.label != EAD_ZEROCONF_LABEL || ead_1.value.is_none() {
+            return Err(EDHOCError::EADError);
+        }
+
+        let (loc_w, _enc_id) = parse_ead_1_value(&ead_1.value.unwrap())?;
+        let voucher_request = encode_voucher_request(message_1, &opaque_state);
+
+        Ok((
+            loc_w,
+            voucher_request,
+            ZeroTouchAuthenticatorWaitVoucherResp::default(),
+        ))
+    }
+}
+
+impl ZeroTouchAuthenticatorWaitVoucherResp {
+    pub fn prepare_ead_2(
+        &self,
+        voucher_response: &EdhocMessageBuffer,
+    ) -> Result<EADItem, EDHOCError> {
+        let (_message_1, voucher, _opaque_state) = parse_voucher_response(&voucher_response)?;
+
+        Ok(EADItem {
+            label: EAD_ZEROCONF_LABEL,
+            is_critical: true,
+            value: Some(voucher[..].try_into().unwrap()),
+        })
+    }
+}
+
+pub fn encode_voucher_request(
+    message_1: &EdhocMessageBuffer,
+    opaque_state: &Option<EdhocMessageBuffer>,
+) -> EdhocMessageBuffer {
+    let mut output = EdhocMessageBuffer::new();
+
+    output.content[1] = CBOR_BYTE_STRING;
+    output.content[2] = message_1.len as u8;
+    output.content[3..3 + message_1.len].copy_from_slice(message_1.as_slice());
+
+    if let Some(opaque_state) = opaque_state {
+        output.content[0] = CBOR_MAJOR_ARRAY | 2;
+
+        output.content[3 + message_1.len] = CBOR_BYTE_STRING;
+        output.content[4 + message_1.len] = opaque_state.len as u8;
+        output.content[5 + message_1.len..5 + message_1.len + opaque_state.len]
+            .copy_from_slice(opaque_state.as_slice());
+
+        output.len = 5 + message_1.len + opaque_state.len;
+    } else {
+        output.content[0] = CBOR_MAJOR_ARRAY | 1;
+        output.len = 3 + message_1.len;
+    }
+
+    output
+}
+
+fn parse_voucher_response(
+    voucher_response: &EdhocMessageBuffer,
+) -> Result<
+    (
+        EdhocMessageBuffer,
+        BytesEncodedVoucher,
+        Option<EdhocMessageBuffer>,
+    ),
+    EDHOCError,
+> {
+    let mut decoder = CBORDecoder::new(voucher_response.as_slice());
+
+    let array_size = decoder.array()?;
+    if !(2..=3).contains(&array_size) {
+        return Err(EDHOCError::EADError);
+    }
+
+    let message_1: EdhocMessageBuffer = decoder.bytes()?.try_into().unwrap();
+    let voucher: BytesEncodedVoucher = decoder
+        .bytes_sized(ENCODED_VOUCHER_LEN)?
+        .try_into()
+        .unwrap();
+
+    if array_size == 3 {
+        let opaque_state: EdhocMessageBuffer = decoder.bytes()?.try_into().unwrap();
+        return Ok((message_1, voucher, Some(opaque_state)));
+    } else {
+        return Ok((message_1, voucher, None));
+    }
+}
+
+#[cfg(test)]
+mod test_authenticator {
+    use super::*;
+    use crate::test_vectors::*;
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_parse_ead_1_value() {
+        let loc_w_tv: EdhocMessageBuffer = LOC_W_TV.try_into().unwrap();
+        let enc_id_tv: EdhocMessageBuffer = ENC_ID_TV.try_into().unwrap();
+
+        let res = parse_ead_1_value(&EAD1_VALUE_TV.try_into().unwrap());
+        assert!(res.is_ok());
+        let (loc_w, enc_id) = res.unwrap();
+        assert_eq!(loc_w.content, loc_w_tv.content);
+        assert_eq!(enc_id.content, enc_id_tv.content);
+    }
+
+    #[test]
+    fn test_encode_voucher_request() {
+        let voucher_request_tv: EdhocMessageBuffer = VOUCHER_REQUEST_TV.try_into().unwrap();
+
+        let voucher_request =
+            encode_voucher_request(&MESSAGE_1_WITH_EAD_TV.try_into().unwrap(), &None);
+        assert_eq!(voucher_request.content, voucher_request_tv.content);
+    }
+
+    #[test]
+    fn test_process_ead_1() {
+        let ead_1 = EADItem {
+            label: EAD_ZEROCONF_LABEL,
+            is_critical: true,
+            value: Some(EAD1_VALUE_TV.try_into().unwrap()),
+        };
+
+        let ead_authenticator = ZeroTouchAuthenticator::default();
+        let res =
+            ead_authenticator.process_ead_1(&ead_1, &MESSAGE_1_WITH_EAD_TV.try_into().unwrap());
+        assert!(res.is_ok());
+    }
+
+    #[test]
+    fn test_parse_voucher_response() {
+        let voucher_response_tv: EdhocMessageBuffer = VOUCHER_RESPONSE_TV.try_into().unwrap();
+        let message_1_tv: EdhocMessageBuffer = MESSAGE_1_WITH_EAD_TV.try_into().unwrap();
+        let voucher_tv: BytesEncodedVoucher = VOUCHER_TV.try_into().unwrap();
+
+        let res = parse_voucher_response(&voucher_response_tv);
+        assert!(res.is_ok());
+        let (message_1, voucher, opaque_state) = res.unwrap();
+        assert_eq!(message_1.content, message_1_tv.content);
+        assert_eq!(voucher, voucher_tv);
+        assert!(opaque_state.is_none());
+    }
+
+    #[test]
+    fn test_r_prepare_ead_2() {
+        let voucher_response_tv: EdhocMessageBuffer = VOUCHER_RESPONSE_TV.try_into().unwrap();
+        let ead_2_value_tv: EdhocMessageBuffer = EAD2_VALUE_TV.try_into().unwrap();
+
+        let ead_authenticator = ZeroTouchAuthenticatorWaitVoucherResp::default();
+
+        let ead_2 = ead_authenticator
+            .prepare_ead_2(&voucher_response_tv)
+            .unwrap();
+        assert_eq!(ead_2.label, EAD_ZEROCONF_LABEL);
+        assert_eq!(ead_2.is_critical, true);
+        assert_eq!(ead_2.value.unwrap().content, ead_2_value_tv.content);
+    }
+}
+
+#[cfg(test)]
+mod test_responder_stateless_operation {
+    use super::*;
+    use crate::test_vectors::*;
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_slo_encode_voucher_request() {
+        let message_1_tv: EdhocMessageBuffer = MESSAGE_1_WITH_EAD_TV.try_into().unwrap();
+        let opaque_state_tv: EdhocMessageBuffer = SLO_OPAQUE_STATE_TV.try_into().unwrap();
+        let voucher_request_tv: EdhocMessageBuffer = SLO_VOUCHER_REQUEST_TV.try_into().unwrap();
+
+        let voucher_request = encode_voucher_request(&message_1_tv, &Some(opaque_state_tv));
+        assert_eq!(voucher_request.content, voucher_request_tv.content);
+    }
+
+    #[test]
+    fn test_slo_parse_voucher_response() {
+        let voucher_response_tv: EdhocMessageBuffer = SLO_VOUCHER_RESPONSE_TV.try_into().unwrap();
+        let message_1_tv: EdhocMessageBuffer = MESSAGE_1_WITH_EAD_TV.try_into().unwrap();
+        let voucher_tv: BytesEncodedVoucher = VOUCHER_TV.try_into().unwrap();
+        let opaque_state_tv: EdhocMessageBuffer = SLO_OPAQUE_STATE_TV.try_into().unwrap();
+
+        let res = parse_voucher_response(&voucher_response_tv);
+        assert!(res.is_ok());
+        let (message_1, voucher, opaque_state) = res.unwrap();
+        assert_eq!(message_1.content, message_1_tv.content);
+        assert_eq!(voucher, voucher_tv);
+        assert_eq!(opaque_state.unwrap().content, opaque_state_tv.content);
+    }
+}
diff --git a/ead/lakers-ead-authz/src/device.rs b/ead/lakers-ead-authz/src/device.rs
new file mode 100644
index 00000000..c3d8e99a
--- /dev/null
+++ b/ead/lakers-ead-authz/src/device.rs
@@ -0,0 +1,194 @@
+use super::shared::*;
+use lakers_shared::{Crypto as CryptoTrait, *};
+
+#[derive(Debug)]
+pub struct ZeroTouchDevice {
+    pub(crate) id_u: EdhocMessageBuffer, // identifier of the device (U), equivalent to ID_CRED_I in EDHOC
+    pub(crate) g_w: BytesP256ElemLen,    // public key of the enrollment server (W)
+    pub(crate) loc_w: EdhocMessageBuffer, // address of the enrollment server (W)
+}
+
+#[derive(Debug)]
+pub struct ZeroTouchDeviceWaitEAD2 {
+    prk: BytesHashLen,
+}
+
+#[derive(Debug)]
+pub struct ZeroTouchDeviceDone {
+    pub voucher: BytesMac,
+}
+
+impl ZeroTouchDevice {
+    pub fn new(id_u: EdhocMessageBuffer, g_w: BytesP256ElemLen, loc_w: EdhocMessageBuffer) -> Self {
+        ZeroTouchDevice { id_u, g_w, loc_w }
+    }
+
+    pub fn prepare_ead_1<Crypto: CryptoTrait>(
+        &self,
+        crypto: &mut Crypto,
+        x: &BytesP256ElemLen,
+        ss: u8,
+    ) -> (EADItem, ZeroTouchDeviceWaitEAD2) {
+        // PRK = EDHOC-Extract(salt, IKM)
+        let prk = compute_prk(crypto, x, &self.g_w);
+
+        // plaintext = (ID_U: bstr)
+        let encoded_id_u = encode_id_u(&self.id_u);
+        let enc_id = encrypt_enc_id(crypto, &prk, &encoded_id_u, ss);
+        let value = Some(encode_ead_1_value(&self.loc_w, &enc_id));
+
+        let ead_1 = EADItem {
+            label: EAD_ZEROCONF_LABEL,
+            is_critical: true,
+            value,
+        };
+
+        (ead_1, ZeroTouchDeviceWaitEAD2 { prk })
+    }
+}
+
+impl ZeroTouchDeviceWaitEAD2 {
+    pub fn process_ead_2<Crypto: CryptoTrait>(
+        &self,
+        crypto: &mut Crypto,
+        ead_2: EADItem,
+        cred_v: &[u8],
+        h_message_1: &BytesHashLen,
+    ) -> Result<ZeroTouchDeviceDone, ()> {
+        if ead_2.label != EAD_ZEROCONF_LABEL || ead_2.value.is_none() {
+            return Err(());
+        }
+        let mut ead_2_value: BytesEncodedVoucher = Default::default();
+        ead_2_value[..].copy_from_slice(&ead_2.value.unwrap().content[..ENCODED_VOUCHER_LEN]);
+
+        match verify_voucher(crypto, &ead_2_value, h_message_1, cred_v, &self.prk) {
+            Ok(voucher) => Ok(ZeroTouchDeviceDone { voucher }),
+            Err(_) => Err(()),
+        }
+    }
+}
+
+fn encode_id_u(id_u: &EdhocMessageBuffer) -> EdhocMessageBuffer {
+    // plaintext = (ID_U: bstr)
+    let mut plaintext = EdhocMessageBuffer::new();
+    plaintext.content[0] = CBOR_MAJOR_BYTE_STRING + id_u.len as u8;
+    plaintext.content[1..1 + id_u.len].copy_from_slice(id_u.as_slice());
+    plaintext.len = 1 + id_u.len;
+
+    plaintext
+}
+
+fn encrypt_enc_id<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    prk: &BytesHashLen,
+    plaintext: &EdhocMessageBuffer,
+    ss: u8,
+) -> EdhocMessageBuffer {
+    let (k_1, iv_1) = compute_k_1_iv_1(crypto, &prk);
+
+    // external_aad = (SS: int)
+    let enc_structure = encode_enc_structure(ss);
+
+    // ENC_ID = 'ciphertext' of COSE_Encrypt0
+    crypto.aes_ccm_encrypt_tag_8(&k_1, &iv_1, &enc_structure[..], plaintext)
+}
+
+fn encode_ead_1_value(
+    loc_w: &EdhocMessageBuffer,
+    enc_id: &EdhocMessageBuffer,
+) -> EdhocMessageBuffer {
+    let mut output = EdhocMessageBuffer::new();
+
+    output.content[0] = CBOR_BYTE_STRING;
+    // put length at output.content[1] after other sizes are known
+
+    output.content[2] = CBOR_TEXT_STRING;
+    output.content[3] = loc_w.len as u8;
+    output.content[4..4 + loc_w.len].copy_from_slice(loc_w.as_slice());
+
+    output.content[4 + loc_w.len] = CBOR_MAJOR_BYTE_STRING + enc_id.len as u8;
+    output.content[5 + loc_w.len..5 + loc_w.len + enc_id.len].copy_from_slice(enc_id.as_slice());
+
+    output.len = 5 + loc_w.len + enc_id.len;
+    output.content[1] = (output.len - 2) as u8;
+
+    output
+}
+
+#[cfg(test)]
+mod test_device {
+    use super::*;
+    use crate::test_vectors::*;
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_encrypt_enc_id() {
+        let enc_id_tv: EdhocMessageBuffer = ENC_ID_TV.try_into().unwrap();
+
+        let enc_id = encrypt_enc_id(
+            &mut default_crypto(),
+            &PRK_TV.try_into().unwrap(),
+            &ID_U_ENCODED_TV.try_into().unwrap(),
+            SS_TV,
+        );
+        assert_eq!(enc_id.content, enc_id_tv.content);
+    }
+
+    #[test]
+    fn test_prepare_ead_1() {
+        let ead_1_value_tv: EdhocMessageBuffer = EAD1_VALUE_TV.try_into().unwrap();
+
+        let ead_device = ZeroTouchDevice::new(
+            ID_U_TV.try_into().unwrap(),
+            G_W_TV.try_into().unwrap(),
+            LOC_W_TV.try_into().unwrap(),
+        );
+
+        let (ead_1, _ead_authz) =
+            ead_device.prepare_ead_1(&mut default_crypto(), &X_TV.try_into().unwrap(), SS_TV);
+        assert_eq!(ead_1.label, EAD_ZEROCONF_LABEL);
+        assert_eq!(ead_1.is_critical, true);
+        assert_eq!(ead_1.value.unwrap().content, ead_1_value_tv.content);
+    }
+
+    #[test]
+    fn test_verify_voucher() {
+        let voucher_tv = VOUCHER_TV.try_into().unwrap();
+        let h_message_1_tv = H_MESSAGE_1_TV.try_into().unwrap();
+        let prk_tv = PRK_TV.try_into().unwrap();
+        let voucher_mac_tv: BytesMac = VOUCHER_MAC_TV.try_into().unwrap();
+
+        let res = verify_voucher(
+            &mut default_crypto(),
+            &voucher_tv,
+            &h_message_1_tv,
+            &CRED_V_TV,
+            &prk_tv,
+        );
+        assert!(res.is_ok());
+        assert_eq!(res.unwrap(), voucher_mac_tv);
+    }
+
+    #[test]
+    fn test_process_ead_2() {
+        let ead_2_tv = EADItem {
+            label: EAD_ZEROCONF_LABEL,
+            is_critical: true,
+            value: Some(EAD2_VALUE_TV.try_into().unwrap()),
+        };
+
+        let ead_device = ZeroTouchDeviceWaitEAD2 {
+            prk: PRK_TV.try_into().unwrap(),
+        };
+
+        let res = ead_device.process_ead_2(
+            &mut default_crypto(),
+            ead_2_tv,
+            CRED_V_TV.try_into().unwrap(),
+            &H_MESSAGE_1_TV.try_into().unwrap(),
+        );
+        assert!(res.is_ok());
+        let ead_device = res.unwrap();
+        assert_eq!(ead_device.voucher, VOUCHER_MAC_TV); // TODO: maybe should use the encoded voucher instead?
+    }
+}
diff --git a/ead/lakers-ead-authz/src/lib.rs b/ead/lakers-ead-authz/src/lib.rs
new file mode 100644
index 00000000..9e8d7f8e
--- /dev/null
+++ b/ead/lakers-ead-authz/src/lib.rs
@@ -0,0 +1,65 @@
+#![no_std]
+
+mod authenticator;
+mod device;
+mod server;
+mod shared;
+#[cfg(test)]
+mod test_vectors;
+
+pub use authenticator::ZeroTouchAuthenticator;
+pub use device::ZeroTouchDevice;
+pub use server::ZeroTouchServer;
+
+#[cfg(test)]
+mod test_authz {
+    use crate::{
+        authenticator::ZeroTouchAuthenticator, device::ZeroTouchDevice, server::ZeroTouchServer,
+        test_vectors::*,
+    };
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_complete_flow() {
+        let device = ZeroTouchDevice::new(
+            ID_U_TV.try_into().unwrap(),
+            G_W_TV.try_into().unwrap(),
+            LOC_W_TV.try_into().unwrap(),
+        );
+        let authenticator = ZeroTouchAuthenticator::default();
+        let server = ZeroTouchServer::new(
+            W_TV.try_into().unwrap(),
+            CRED_V_TV.try_into().unwrap(),
+            Some(ACL_TV.try_into().unwrap()),
+        );
+
+        // using .unwrap below since detailed errors are tested in each entity's tests
+
+        let (ead_1, device) =
+            device.prepare_ead_1(&mut default_crypto(), &X_TV.try_into().unwrap(), SS_TV);
+
+        // ead_1 will be transported within message_1
+
+        let (_loc_w, voucher_request, authenticator) = authenticator
+            .process_ead_1(&ead_1, &MESSAGE_1_WITH_EAD_TV.try_into().unwrap())
+            .unwrap();
+
+        // network request would be: let Ok(voucher_response) = auth_client.post(loc_w, voucher_request)
+
+        let voucher_response = server
+            .handle_voucher_request(&mut default_crypto(), &voucher_request)
+            .unwrap();
+
+        let ead_2 = authenticator.prepare_ead_2(&voucher_response).unwrap();
+
+        // ead_2 will be transported within message_2
+
+        let result = device.process_ead_2(
+            &mut default_crypto(),
+            ead_2,
+            CRED_V_TV,
+            H_MESSAGE_1_TV.try_into().unwrap(),
+        );
+        assert!(result.is_ok());
+    }
+}
diff --git a/ead/lakers-ead-authz/src/server.rs b/ead/lakers-ead-authz/src/server.rs
new file mode 100644
index 00000000..93adcaf1
--- /dev/null
+++ b/ead/lakers-ead-authz/src/server.rs
@@ -0,0 +1,270 @@
+use super::shared::*;
+use lakers_shared::{Crypto as CryptoTrait, *};
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct ZeroTouchServer {
+    w: BytesP256ElemLen,            // private key of the enrollment server (W)
+    pub cred_v: EdhocMessageBuffer, // credential of the authenticator (V)
+    // access control list, each device identified by an u8 kid (this is arbitrary, it is not specified in the draft)
+    pub acl: Option<EdhocMessageBuffer>,
+}
+
+impl ZeroTouchServer {
+    pub fn new(w: BytesP256ElemLen, cred_v: &[u8], acl: Option<EdhocMessageBuffer>) -> Self {
+        let cred_v: EdhocMessageBuffer = cred_v.try_into().unwrap();
+        ZeroTouchServer { w, cred_v, acl }
+    }
+
+    pub fn authorized(self, kid: u8) -> bool {
+        if let Some(acl) = self.acl {
+            acl.content.contains(&kid)
+        } else {
+            // if no acl then allow it
+            true
+        }
+    }
+
+    pub fn handle_voucher_request<Crypto: CryptoTrait>(
+        &self,
+        crypto: &mut Crypto,
+        vreq: &EdhocMessageBuffer,
+    ) -> Result<EdhocMessageBuffer, EDHOCError> {
+        let (message_1, opaque_state) = parse_voucher_request(vreq)?;
+
+        let (_method, _suites_i, _suites_i_len, g_x, _c_i, ead_1) = parse_message_1(&message_1)?;
+
+        // compute hash
+        let mut message_1_buf: BytesMaxBuffer = [0x00; MAX_BUFFER_LEN];
+        message_1_buf[..message_1.len].copy_from_slice(message_1.as_slice());
+        let h_message_1 = crypto.sha256_digest(&message_1_buf, message_1.len);
+
+        let prk = compute_prk(crypto, &self.w, &g_x);
+
+        let (_loc_w, enc_id) = parse_ead_1_value(&ead_1.unwrap().value.unwrap())?;
+        let id_u_encoded = decrypt_enc_id(crypto, &prk, &enc_id, EDHOC_SUPPORTED_SUITES[0])?;
+        let id_u = decode_id_u(id_u_encoded)?;
+
+        if self.acl.is_none() || self.authorized(id_u.content[3]) {
+            let voucher = prepare_voucher(crypto, &h_message_1, &self.cred_v.as_slice(), &prk);
+            let voucher_response = encode_voucher_response(&message_1, &voucher, &opaque_state);
+            Ok(voucher_response)
+        } else {
+            Err(EDHOCError::EADError)
+        }
+    }
+}
+
+fn parse_voucher_request(
+    vreq: &EdhocMessageBuffer,
+) -> Result<(EdhocMessageBuffer, Option<EdhocMessageBuffer>), EDHOCError> {
+    let mut decoder = CBORDecoder::new(vreq.as_slice());
+    let array_size = decoder.array()?;
+    if array_size != 1 && array_size != 2 {
+        return Err(EDHOCError::EADError);
+    }
+
+    let message_1: EdhocMessageBuffer = decoder.bytes()?.try_into().unwrap();
+
+    if array_size == 2 {
+        let opaque_state: EdhocMessageBuffer = decoder.bytes()?.try_into().unwrap();
+        Ok((message_1, Some(opaque_state)))
+    } else {
+        Ok((message_1, None))
+    }
+}
+
+fn decrypt_enc_id<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    prk: &BytesHashLen,
+    enc_id: &EdhocMessageBuffer,
+    ss: u8,
+) -> Result<EdhocMessageBuffer, EDHOCError> {
+    let (k_1, iv_1) = compute_k_1_iv_1(crypto, &prk);
+
+    // external_aad = (SS: int)
+    let enc_structure = encode_enc_structure(ss);
+
+    // ENC_ID = 'ciphertext' of COSE_Encrypt0
+    crypto.aes_ccm_decrypt_tag_8(&k_1, &iv_1, &enc_structure[..], &enc_id)
+}
+
+fn decode_id_u(id_u_bstr: EdhocMessageBuffer) -> Result<EdhocMessageBuffer, EDHOCError> {
+    // id_u is encoded as bstr
+    let mut decoder = CBORDecoder::new(id_u_bstr.as_slice());
+    let id_u: EdhocMessageBuffer = decoder.bytes()?.try_into().unwrap();
+    Ok(id_u)
+}
+
+fn encode_voucher_response(
+    message_1: &EdhocMessageBuffer,
+    voucher: &BytesEncodedVoucher,
+    opaque_state: &Option<EdhocMessageBuffer>,
+) -> EdhocMessageBuffer {
+    let mut output = EdhocMessageBuffer::new();
+
+    output.content[1] = CBOR_BYTE_STRING;
+    output.content[2] = message_1.len as u8;
+    output.content[3..3 + message_1.len].copy_from_slice(message_1.as_slice());
+
+    output.content[3 + message_1.len] = CBOR_MAJOR_BYTE_STRING + ENCODED_VOUCHER_LEN as u8;
+    output.content[4 + message_1.len..4 + message_1.len + ENCODED_VOUCHER_LEN]
+        .copy_from_slice(&voucher[..]);
+
+    if let Some(opaque_state) = opaque_state {
+        output.content[0] = CBOR_MAJOR_ARRAY | 3;
+
+        output.content[4 + message_1.len + ENCODED_VOUCHER_LEN] = CBOR_BYTE_STRING;
+        output.content[5 + message_1.len + ENCODED_VOUCHER_LEN] = opaque_state.len as u8;
+        output.content[6 + message_1.len + ENCODED_VOUCHER_LEN
+            ..6 + message_1.len + ENCODED_VOUCHER_LEN + opaque_state.len]
+            .copy_from_slice(opaque_state.as_slice());
+
+        output.len = 6 + message_1.len + ENCODED_VOUCHER_LEN + opaque_state.len;
+    } else {
+        output.content[0] = CBOR_MAJOR_ARRAY | 2;
+        output.len = 4 + message_1.len + ENCODED_VOUCHER_LEN;
+    }
+
+    output
+}
+
+#[cfg(test)]
+mod test_enrollment_server {
+    use super::*;
+    use crate::test_vectors::*;
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_decrypt_enc_id() {
+        let mut prk_tv: BytesHashLen = Default::default();
+        prk_tv[..].copy_from_slice(PRK_TV);
+        let id_u_encoded_tv: EdhocMessageBuffer = ID_U_ENCODED_TV.try_into().unwrap();
+
+        let id_u_res = decrypt_enc_id(
+            &mut default_crypto(),
+            &prk_tv,
+            &ENC_ID_TV.try_into().unwrap(),
+            SS_TV,
+        );
+        assert!(id_u_res.is_ok());
+        assert_eq!(id_u_res.unwrap().content, id_u_encoded_tv.content);
+    }
+
+    #[test]
+    fn test_prepare_voucher() {
+        let h_message_1: BytesHashLen = H_MESSAGE_1_TV.try_into().unwrap();
+        let prk: BytesHashLen = PRK_TV.try_into().unwrap();
+        let voucher_tv: BytesEncodedVoucher = VOUCHER_TV.try_into().unwrap();
+
+        let voucher = prepare_voucher(&mut default_crypto(), &h_message_1, &CRED_V_TV, &prk);
+        assert_eq!(voucher, voucher_tv);
+    }
+
+    #[test]
+    fn test_encode_voucher_response() {
+        let message_1_tv: EdhocMessageBuffer = MESSAGE_1_WITH_EAD_TV.try_into().unwrap();
+        let voucher_tv: BytesEncodedVoucher = VOUCHER_TV.try_into().unwrap();
+        let opaque_state_tv: EdhocMessageBuffer = SLO_OPAQUE_STATE_TV.try_into().unwrap();
+        let voucher_response_tv: EdhocMessageBuffer = SLO_VOUCHER_RESPONSE_TV.try_into().unwrap();
+
+        let voucher_response =
+            encode_voucher_response(&message_1_tv, &voucher_tv, &Some(opaque_state_tv));
+        assert_eq!(voucher_response.content, voucher_response_tv.content);
+    }
+
+    #[test]
+    fn test_parse_voucher_request() {
+        let voucher_request_tv: EdhocMessageBuffer = VOUCHER_REQUEST_TV.try_into().unwrap();
+        let message_1_tv: EdhocMessageBuffer = MESSAGE_1_WITH_EAD_TV.try_into().unwrap();
+
+        let voucher_request = parse_voucher_request(&voucher_request_tv);
+        assert!(voucher_request.is_ok());
+        let (message_1, opaque_state) = voucher_request.unwrap();
+        assert_eq!(message_1.content, message_1_tv.content);
+        assert!(opaque_state.is_none());
+    }
+
+    #[test]
+    fn test_handle_voucher_request_acl_none() {
+        let voucher_response_tv: EdhocMessageBuffer = VOUCHER_RESPONSE_TV.try_into().unwrap();
+
+        let ead_server = ZeroTouchServer::new(W_TV.try_into().unwrap(), CRED_V_TV, None);
+
+        let res = ead_server.handle_voucher_request(
+            &mut default_crypto(),
+            &VOUCHER_REQUEST_TV.try_into().unwrap(),
+        );
+        assert!(res.is_ok());
+        let voucher_response = res.unwrap();
+        assert_eq!(voucher_response.content, voucher_response_tv.content);
+    }
+
+    #[test]
+    fn test_handle_voucher_request_acl_ok() {
+        let voucher_response_tv: EdhocMessageBuffer = VOUCHER_RESPONSE_TV.try_into().unwrap();
+
+        let ead_server = ZeroTouchServer::new(
+            W_TV.try_into().unwrap(),
+            CRED_V_TV,
+            Some(ACL_TV.try_into().unwrap()),
+        );
+
+        let res = ead_server.handle_voucher_request(
+            &mut default_crypto(),
+            &VOUCHER_REQUEST_TV.try_into().unwrap(),
+        );
+        assert!(res.is_ok());
+        let voucher_response = res.unwrap();
+        assert_eq!(voucher_response.content, voucher_response_tv.content);
+    }
+
+    #[test]
+    fn test_handle_voucher_request_acl_invalid() {
+        let ead_server = ZeroTouchServer::new(
+            W_TV.try_into().unwrap(),
+            CRED_V_TV,
+            Some(ACL_INVALID_TV.try_into().unwrap()),
+        );
+
+        let res = ead_server.handle_voucher_request(
+            &mut default_crypto(),
+            &VOUCHER_REQUEST_TV.try_into().unwrap(),
+        );
+        assert_eq!(res.unwrap_err(), EDHOCError::EADError);
+    }
+}
+
+#[cfg(test)]
+mod test_server_stateless_operation {
+    use super::*;
+    use crate::test_vectors::*;
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_slo_parse_voucher_request() {
+        let voucher_request_tv: EdhocMessageBuffer = SLO_VOUCHER_REQUEST_TV.try_into().unwrap();
+        let message_1_tv: EdhocMessageBuffer = MESSAGE_1_WITH_EAD_TV.try_into().unwrap();
+        let opaque_state_tv: EdhocMessageBuffer = SLO_OPAQUE_STATE_TV.try_into().unwrap();
+
+        let voucher_request = parse_voucher_request(&voucher_request_tv);
+        assert!(voucher_request.is_ok());
+        let (message_1, opaque_state) = voucher_request.unwrap();
+        assert_eq!(message_1.content, message_1_tv.content);
+        assert_eq!(opaque_state.unwrap().content, opaque_state_tv.content);
+    }
+
+    #[test]
+    fn test_slo_handle_voucher_request() {
+        let voucher_response_tv: EdhocMessageBuffer = SLO_VOUCHER_RESPONSE_TV.try_into().unwrap();
+
+        let ead_server = ZeroTouchServer::new(W_TV.try_into().unwrap(), CRED_V_TV, None);
+
+        let res = ead_server.handle_voucher_request(
+            &mut default_crypto(),
+            &SLO_VOUCHER_REQUEST_TV.try_into().unwrap(),
+        );
+        assert!(res.is_ok());
+        let voucher_response = res.unwrap();
+        assert_eq!(voucher_response.content, voucher_response_tv.content);
+    }
+}
diff --git a/ead/lakers-ead-authz/src/shared.rs b/ead/lakers-ead-authz/src/shared.rs
new file mode 100644
index 00000000..504563a0
--- /dev/null
+++ b/ead/lakers-ead-authz/src/shared.rs
@@ -0,0 +1,217 @@
+use lakers_shared::{Crypto as CryptoTrait, *};
+
+pub(crate) fn compute_prk<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    a: &BytesP256ElemLen,
+    g_b: &BytesP256ElemLen,
+) -> BytesHashLen {
+    // NOTE: salt should be h'' (the zero-length byte string), but crypto backends are hardcoded to salts of size SHA256_DIGEST_LEN (32).
+    //       nevertheless, using a salt of HashLen zeros works as well (see RFC 5869, Section 2.2).
+    let salt: BytesHashLen = [0u8; SHA256_DIGEST_LEN];
+    let g_ab = crypto.p256_ecdh(a, g_b);
+    crypto.hkdf_extract(&salt, &g_ab)
+}
+
+pub(crate) fn verify_voucher<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    received_voucher: &BytesEncodedVoucher,
+    h_message_1: &BytesHashLen,
+    cred_v: &[u8],
+    prk: &BytesHashLen,
+) -> Result<BytesMac, ()> {
+    let prepared_voucher = &prepare_voucher(crypto, h_message_1, cred_v, prk);
+    if received_voucher == prepared_voucher {
+        let mut voucher_mac: BytesMac = Default::default();
+        voucher_mac[..MAC_LENGTH].copy_from_slice(&prepared_voucher[1..1 + MAC_LENGTH]);
+        return Ok(voucher_mac);
+    } else {
+        return Err(());
+    }
+}
+
+pub(crate) fn prepare_voucher<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    h_message_1: &BytesHashLen,
+    cred_v: &[u8],
+    prk: &BytesP256ElemLen,
+) -> BytesEncodedVoucher {
+    let voucher_input = encode_voucher_input(&h_message_1, &cred_v);
+    let voucher_mac = compute_voucher_mac(crypto, &prk, &voucher_input);
+    encode_voucher(&voucher_mac)
+}
+
+pub(crate) fn compute_k_1_iv_1<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    prk: &BytesHashLen,
+) -> (BytesCcmKeyLen, BytesCcmIvLen) {
+    // K_1 = EDHOC-Expand(PRK, info = (0, h'', AES_CCM_KEY_LEN), length)
+    let mut k_1: BytesCcmKeyLen = [0x00; AES_CCM_KEY_LEN];
+    let k_1_buf = edhoc_kdf_expand(
+        crypto,
+        prk,
+        EAD_ZEROCONF_INFO_K_1_LABEL,
+        &[0x00; MAX_KDF_CONTEXT_LEN],
+        0,
+        AES_CCM_KEY_LEN,
+    );
+    k_1[..].copy_from_slice(&k_1_buf[..AES_CCM_KEY_LEN]);
+
+    // IV_1 = EDHOC-Expand(PRK, info = (1, h'', AES_CCM_IV_LEN), length)
+    let mut iv_1: BytesCcmIvLen = [0x00; AES_CCM_IV_LEN];
+    let iv_1_buf = edhoc_kdf_expand(
+        crypto,
+        prk,
+        EAD_ZEROCONF_INFO_IV_1_LABEL,
+        &[0x00; MAX_KDF_CONTEXT_LEN],
+        0,
+        AES_CCM_IV_LEN,
+    );
+    iv_1[..].copy_from_slice(&iv_1_buf[..AES_CCM_IV_LEN]);
+
+    (k_1, iv_1)
+}
+
+pub(crate) fn parse_ead_1_value(
+    value: &EdhocMessageBuffer,
+) -> Result<(EdhocMessageBuffer, EdhocMessageBuffer), EDHOCError> {
+    let mut outer_decoder = CBORDecoder::new(value.as_slice());
+    let voucher_info_seq = outer_decoder.bytes()?;
+    let mut seq_decoder = CBORDecoder::new(voucher_info_seq);
+    Ok((
+        seq_decoder.str()?.try_into().unwrap(),
+        seq_decoder.bytes()?.try_into().unwrap(),
+    ))
+}
+
+pub(crate) fn encode_enc_structure(ss: u8) -> [u8; EAD_ZEROCONF_ENC_STRUCTURE_LEN] {
+    let mut encrypt0: Bytes8 = [0x00; 8];
+    encrypt0[0] = 0x45u8; // 'E'
+    encrypt0[1] = 0x6eu8; // 'n'
+    encrypt0[2] = 0x63u8; // 'c'
+    encrypt0[3] = 0x72u8; // 'r'
+    encrypt0[4] = 0x79u8; // 'y'
+    encrypt0[5] = 0x70u8; // 'p'
+    encrypt0[6] = 0x74u8; // 't'
+    encrypt0[7] = 0x30u8; // '0'
+
+    let mut enc_structure: [u8; EAD_ZEROCONF_ENC_STRUCTURE_LEN] =
+        [0x00; EAD_ZEROCONF_ENC_STRUCTURE_LEN];
+
+    // encode Enc_structure from rfc9052 Section 5.3
+    enc_structure[0] = CBOR_MAJOR_ARRAY | 3 as u8; // 3 is the fixed number of elements in the array
+    enc_structure[1] = CBOR_MAJOR_TEXT_STRING | encrypt0.len() as u8;
+    enc_structure[2..2 + encrypt0.len()].copy_from_slice(&encrypt0[..]);
+    enc_structure[encrypt0.len() + 2] = CBOR_MAJOR_BYTE_STRING | 0x00 as u8; // 0 for zero-length byte string (empty Header)
+    enc_structure[encrypt0.len() + 3] = CBOR_MAJOR_BYTE_STRING | 0x01 as u8; // 1 for the `ss` value
+    enc_structure[encrypt0.len() + 4] = ss;
+
+    enc_structure
+}
+
+// private functions
+
+fn encode_voucher_input(h_message_1: &BytesHashLen, cred_v: &[u8]) -> EdhocMessageBuffer {
+    let mut voucher_input = EdhocMessageBuffer::new();
+
+    voucher_input.content[0] = CBOR_BYTE_STRING;
+    voucher_input.content[1] = SHA256_DIGEST_LEN as u8;
+    voucher_input.content[2..2 + SHA256_DIGEST_LEN]
+        .copy_from_slice(&h_message_1[..SHA256_DIGEST_LEN]);
+
+    voucher_input.content[2 + SHA256_DIGEST_LEN] = CBOR_BYTE_STRING;
+    voucher_input.content[3 + SHA256_DIGEST_LEN] = cred_v.len() as u8;
+    voucher_input.content[4 + SHA256_DIGEST_LEN..4 + SHA256_DIGEST_LEN + cred_v.len()]
+        .copy_from_slice(cred_v);
+
+    voucher_input.len = 4 + SHA256_DIGEST_LEN + cred_v.len();
+
+    voucher_input
+}
+
+fn compute_voucher_mac<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    prk: &BytesHashLen,
+    voucher_input: &EdhocMessageBuffer,
+) -> BytesMac {
+    let mut voucher_mac: BytesMac = [0x00; MAC_LENGTH];
+
+    let mut context = [0x00; MAX_KDF_CONTEXT_LEN];
+    context[..voucher_input.len].copy_from_slice(voucher_input.as_slice());
+
+    let voucher_mac_buf = edhoc_kdf_expand(crypto, prk, 2, &context, voucher_input.len, MAC_LENGTH);
+    voucher_mac[..MAC_LENGTH].copy_from_slice(&voucher_mac_buf[..MAC_LENGTH]);
+
+    voucher_mac
+}
+
+fn encode_voucher(voucher_mac: &BytesMac) -> BytesEncodedVoucher {
+    let mut voucher: BytesEncodedVoucher = Default::default();
+    voucher[0] = CBOR_MAJOR_BYTE_STRING + MAC_LENGTH as u8;
+    voucher[1..1 + MAC_LENGTH].copy_from_slice(&voucher_mac[..MAC_LENGTH]);
+
+    voucher
+}
+
+fn edhoc_kdf_expand<Crypto: CryptoTrait>(
+    crypto: &mut Crypto,
+    prk: &BytesHashLen,
+    label: u8,
+    context: &BytesMaxContextBuffer,
+    context_len: usize,
+    length: usize,
+) -> BytesMaxBuffer {
+    let (info, info_len) = encode_info(label, context, context_len, length);
+    let output = crypto.hkdf_expand(prk, &info, info_len, length);
+    output
+}
+
+#[cfg(test)]
+mod test_shared {
+    use super::*;
+    use crate::test_vectors::*;
+
+    use lakers_crypto::default_crypto;
+
+    #[test]
+    fn test_compute_keys() {
+        let k_1_tv: BytesCcmKeyLen = K_1_TV.try_into().unwrap();
+        let iv_1_tv: BytesCcmIvLen = IV_1_TV.try_into().unwrap();
+        let prk_tv: BytesHashLen = PRK_TV.try_into().unwrap();
+
+        let prk_xw = compute_prk(
+            &mut default_crypto(),
+            &X_TV.try_into().unwrap(),
+            &G_W_TV.try_into().unwrap(),
+        );
+        let prk_wx = compute_prk(
+            &mut default_crypto(),
+            &W_TV.try_into().unwrap(),
+            &G_X_TV.try_into().unwrap(),
+        );
+        assert_eq!(prk_xw, prk_tv);
+        assert_eq!(prk_xw, prk_wx);
+
+        let (k_1, iv_1) = compute_k_1_iv_1(&mut default_crypto(), &prk_xw);
+        assert_eq!(k_1, k_1_tv);
+        assert_eq!(iv_1, iv_1_tv);
+    }
+
+    #[test]
+    fn test_encode_voucher_input() {
+        let h_message_1_tv: BytesHashLen = H_MESSAGE_1_TV.try_into().unwrap();
+        let voucher_input_tv: EdhocMessageBuffer = VOUCHER_INPUT_TV.try_into().unwrap();
+
+        let voucher_input = encode_voucher_input(&h_message_1_tv, &CRED_V_TV);
+        assert_eq!(voucher_input.content, voucher_input_tv.content);
+    }
+
+    #[test]
+    fn test_compute_voucher_mac() {
+        let prk_tv: BytesHashLen = PRK_TV.try_into().unwrap();
+        let voucher_input_tv: EdhocMessageBuffer = VOUCHER_INPUT_TV.try_into().unwrap();
+        let voucher_mac_tv: BytesMac = VOUCHER_MAC_TV.try_into().unwrap();
+
+        let voucher_mac = compute_voucher_mac(&mut default_crypto(), &prk_tv, &voucher_input_tv);
+        assert_eq!(voucher_mac, voucher_mac_tv);
+    }
+}


Test patch:
diff --git a/ead/lakers-ead-authz/src/test_vectors.rs b/ead/lakers-ead-authz/src/test_vectors.rs
new file mode 100644
index 00000000..1a10cb43
--- /dev/null
+++ b/ead/lakers-ead-authz/src/test_vectors.rs
@@ -0,0 +1,55 @@
+use hexlit::hex;
+use lakers_shared::BytesP256ElemLen;
+
+// inputs
+// U
+pub const ID_U_TV: &[u8] = &hex!("a104412b");
+pub const ID_U_ENCODED_TV: &[u8] = &hex!("44a104412b");
+pub const X_TV: BytesP256ElemLen =
+    hex!("368ec1f69aeb659ba37d5a8d45b21bdc0299dceaa8ef235f3ca42ce3530f9525");
+pub const G_X_TV: &[u8] = &hex!("8af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6");
+
+// V
+pub const CRED_V_TV: &[u8] = &hex!("a2026b6578616d706c652e65647508a101a501020241322001215820bbc34960526ea4d32e940cad2a234148ddc21791a12afbcbac93622046dd44f02258204519e257236b2a0ce2023f0931f1f386ca7afda64fcde0108c224c51eabf6072");
+
+// W
+pub const W_TV: &[u8] = &hex!("4E5E15AB35008C15B89E91F9F329164D4AACD53D9923672CE0019F9ACD98573F");
+pub const G_W_TV: &[u8] = &hex!("FFA4F102134029B3B156890B88C9D9619501196574174DCB68A07DB0588E4D41");
+pub const LOC_W_TV: &[u8] = &hex!("636F61703A2F2F656E726F6C6C6D656E742E736572766572"); // coap://enrollment.server
+
+pub const ACL_TV: &[u8] = &hex!("2b"); // kid from ID_U_TV
+pub const ACL_INVALID_TV: &[u8] = &hex!("fe"); // random invalid kid
+
+// computed artifacts
+// EAD_1
+pub const SS_TV: u8 = 2;
+pub const ENC_ID_TV: &[u8] = &hex!("da9784962883c96ed01ff122c3");
+pub const PRK_TV: &[u8] = &hex!("d40f1601b577dbe7827bb3a20e0d16f7231c3a25225c1ed733f9094050d59666");
+pub const K_1_TV: &[u8] = &hex!("6f2a9112801a5011aa33576b5c7862ad");
+pub const IV_1_TV: &[u8] = &hex!("d31bc0d128349f290e79f0bde3");
+pub const EAD1_VALUE_TV: &[u8] =
+    &hex!("58287818636f61703a2f2f656e726f6c6c6d656e742e7365727665724dda9784962883c96ed01ff122c3");
+pub const MESSAGE_1_WITH_EAD_TV: &[u8] = &hex!("0382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6370158287818636f61703a2f2f656e726f6c6c6d656e742e7365727665724dda9784962883c96ed01ff122c3");
+
+// VREQ
+pub const VOUCHER_REQUEST_TV: &[u8] = &hex!("8158520382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6370158287818636f61703a2f2f656e726f6c6c6d656e742e7365727665724dda9784962883c96ed01ff122c3");
+
+// VRES
+pub const VOUCHER_RESPONSE_TV: &[u8] = &hex!("8258520382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6370158287818636f61703a2f2f656e726f6c6c6d656e742e7365727665724dda9784962883c96ed01ff122c34948c783671337f75bd5");
+pub const H_MESSAGE_1_TV: &[u8] =
+    &hex!("a1004dfd2c64777980d9c84f100f93a9cac511ae38f56b2210530c945d186c24");
+pub const VOUCHER_INPUT_TV: &[u8] = &hex!("5820a1004dfd2c64777980d9c84f100f93a9cac511ae38f56b2210530c945d186c24585fa2026b6578616d706c652e65647508a101a501020241322001215820bbc34960526ea4d32e940cad2a234148ddc21791a12afbcbac93622046dd44f02258204519e257236b2a0ce2023f0931f1f386ca7afda64fcde0108c224c51eabf6072");
+pub const VOUCHER_MAC_TV: &[u8] = &hex!("c783671337f75bd5");
+pub const VOUCHER_TV: &[u8] = &hex!("48c783671337f75bd5");
+
+// EAD_2
+pub const EAD2_VALUE_TV: &[u8] = &hex!("48c783671337f75bd5");
+
+// ---- Traces for stateless operation (prefixed with SLO)
+// VREQ
+pub const SLO_OPAQUE_STATE_TV: &[u8] =
+    &hex!("827819666538303a3a623833343a643630623a373936663a38646530198bed");
+pub const SLO_VOUCHER_REQUEST_TV: &[u8] = &hex!("8258520382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6370158287818636f61703a2f2f656e726f6c6c6d656e742e7365727665724dda9784962883c96ed01ff122c3581f827819666538303a3a623833343a643630623a373936663a38646530198bed");
+
+// VRES
+pub const SLO_VOUCHER_RESPONSE_TV: &[u8] = &hex!("8358520382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6370158287818636f61703a2f2f656e726f6c6c6d656e742e7365727665724dda9784962883c96ed01ff122c34948c783671337f75bd5581f827819666538303a3a623833343a643630623a373936663a38646530198bed");