use base64ct::{Base64, Encoding};
use p256::ecdsa::signature::hazmat::{PrehashSigner, PrehashVerifier};
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
pub use p256::ecdsa::{SigningKey as ContentSigningKey, VerifyingKey as ContentVerifyingKey};
use p256::pkcs8::{DecodePrivateKey, DecodePublicKey};
use sha2::{Digest as _, Sha256};
use crate::error::Error;
pub const HEADER_DIGEST: &str = "X-BDEW-DIGEST";
pub const HEADER_SIGNATURE: &str = "X-BDEW-SIGNATURE";
pub const HEADER_CERT: &str = "X-BDEW-CERT";
#[must_use]
pub fn cert_header_value(cert_der: &[u8]) -> String {
format!(":{}:", Base64::encode_string(cert_der))
}
#[derive(Debug, Clone, Copy)]
pub struct SignedInputs<'a> {
pub uri: &'a str,
pub payload: &'a [u8],
pub creation_date_time: &'a str,
pub transaction_id: &'a str,
pub initial_transaction_id: Option<&'a str>,
}
pub fn sign_request(
inputs: &SignedInputs<'_>,
signing_key: &SigningKey,
) -> Result<(String, String), Error> {
let digest = compute_digest(inputs);
let sig: Signature = signing_key
.sign_prehash(&digest)
.map_err(|e| Error::Signature(format!("ECDSA prehash sign: {e}")))?;
Ok((
Base64::encode_string(&digest),
Base64::encode_string(&sig.to_bytes()),
))
}
pub fn verify_request(
inputs: &SignedInputs<'_>,
digest_b64: &str,
signature_b64: &str,
verifying_key: &VerifyingKey,
) -> Result<(), Error> {
let expected = compute_digest(inputs);
let received = Base64::decode_vec(digest_b64)
.map_err(|e| Error::Signature(format!("{HEADER_DIGEST} base64 decode: {e}")))?;
if received.as_slice() != expected {
return Err(Error::Signature(format!(
"{HEADER_DIGEST} does not match the request components"
)));
}
let sig_bytes = Base64::decode_vec(signature_b64)
.map_err(|e| Error::Signature(format!("{HEADER_SIGNATURE} base64 decode: {e}")))?;
let sig = Signature::from_slice(&sig_bytes)
.map_err(|e| Error::Signature(format!("malformed ECDSA signature bytes: {e}")))?;
verifying_key
.verify_prehash(&expected, &sig)
.map_err(|_| Error::Signature("ECDSA signature verification failed".into()))
}
pub fn canonical_json<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, Error> {
serde_jcs::to_string(value)
.map(std::string::String::into_bytes)
.map_err(|e| Error::Signature(format!("RFC 8785 JCS canonicalisation: {e}")))
}
pub fn signing_key_from_pem(pem: &str) -> Result<SigningKey, Error> {
SigningKey::from_pkcs8_pem(pem)
.map_err(|e| Error::Signature(format!("content-security private key: {e}")))
}
pub fn verifying_key_from_pem(pem: &str) -> Result<VerifyingKey, Error> {
VerifyingKey::from_public_key_pem(pem)
.map_err(|e| Error::Signature(format!("content-security public key: {e}")))
}
#[must_use]
pub fn compute_digest(inputs: &SignedInputs<'_>) -> [u8; 32] {
let mut m = Sha256::new();
m.update(sha256(inputs.uri.as_bytes()));
m.update(sha256(inputs.payload));
m.update(sha256(inputs.creation_date_time.as_bytes()));
m.update(sha256(inputs.transaction_id.as_bytes()));
if let Some(initial) = inputs.initial_transaction_id {
m.update(sha256(initial.as_bytes()));
}
m.finalize().into()
}
#[inline]
fn sha256(data: &[u8]) -> [u8; 32] {
Sha256::digest(data).into()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_signing_key() -> SigningKey {
let d: [u8; 32] = [
0xC9, 0xAF, 0xA9, 0xD8, 0x45, 0xBA, 0x75, 0x16, 0x6B, 0x5C, 0x21, 0x57, 0x67, 0xB1,
0xD6, 0x93, 0x4E, 0x50, 0xC3, 0xDB, 0x36, 0xE8, 0x9B, 0x12, 0x7B, 0x8A, 0x62, 0x2B,
0x12, 0x0F, 0x67, 0x21,
];
let secret = p256::SecretKey::from_bytes((&d).into()).expect("valid RFC 6979 scalar");
SigningKey::from(&secret)
}
fn inputs<'a>(uri: &'a str, payload: &'a [u8], dt: &'a str, tx: &'a str) -> SignedInputs<'a> {
SignedInputs {
uri,
payload,
creation_date_time: dt,
transaction_id: tx,
initial_transaction_id: None,
}
}
#[test]
fn roundtrip_no_body() {
let key = test_signing_key();
let vk = *key.verifying_key();
let i = inputs(
"https://msb.example.de/[Post]/steuerbefehl/konfiguration/?locationId=E1234848431",
&[],
"2025-06-01T10:00:00.000Z",
"f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
);
let (d, s) = sign_request(&i, &key).expect("sign");
verify_request(&i, &d, &s, &vk).expect("verify");
}
#[test]
fn roundtrip_with_body() {
let key = test_signing_key();
let vk = *key.verifying_key();
let body =
br#"{"identificationDateTime":"2025-06-01T22:00:00Z","energyDirection":"consumption"}"#;
let i = inputs(
"https://nb.example.de/maloId/request/v1",
body,
"2025-06-01T10:00:00.000Z",
"aabbccdd-eeff-0011-2233-445566778899",
);
let (d, s) = sign_request(&i, &key).expect("sign");
verify_request(&i, &d, &s, &vk).expect("verify");
}
#[test]
fn digest_mismatch_is_rejected() {
let key = test_signing_key();
let vk = *key.verifying_key();
let (digest_b64, sig_b64) = sign_request(
&inputs(
"https://example.de/api?x=1",
b"body",
"2025-06-01T10:00:00Z",
"tx-000",
),
&key,
)
.expect("sign");
assert!(
verify_request(
&inputs(
"https://example.de/api?x=TAMPERED",
b"body",
"2025-06-01T10:00:00Z",
"tx-000"
),
&digest_b64,
&sig_b64,
&vk,
)
.is_err()
);
}
#[test]
fn signature_mismatch_is_rejected() {
let key = test_signing_key();
let vk = *key.verifying_key();
let i = inputs(
"https://example.de/api",
&[],
"2025-06-01T10:00:00Z",
"tx-000",
);
let (digest_b64, _) = sign_request(&i, &key).expect("sign");
let bad_sig = Base64::encode_string(&[0u8; 64]);
assert!(verify_request(&i, &digest_b64, &bad_sig, &vk).is_err());
}
#[test]
fn compute_digest_is_deterministic() {
let i = inputs("uri", b"body", "dt", "tx");
assert_eq!(compute_digest(&i), compute_digest(&i));
}
#[test]
fn empty_and_non_empty_payload_differ() {
assert_ne!(
compute_digest(&inputs("uri", &[], "dt", "tx")),
compute_digest(&inputs("uri", b"x", "dt", "tx"))
);
}
#[test]
fn the_initial_transaction_id_is_signed_when_present() {
let without = inputs("uri", b"body", "dt", "tx");
let with = SignedInputs {
initial_transaction_id: Some("first-tx"),
..without
};
assert_ne!(compute_digest(&without), compute_digest(&with));
let mut m = Vec::new();
for part in [&b"uri"[..], b"body", b"dt", b"tx", b"first-tx"] {
m.extend_from_slice(&sha256(part));
}
assert_eq!(
compute_digest(&with),
sha256(&m),
"Hash(M) over the five hashes"
);
let key = test_signing_key();
let (d, s) = sign_request(&with, &key).expect("sign");
let altered = SignedInputs {
initial_transaction_id: Some("other-tx"),
..with
};
assert!(verify_request(&altered, &d, &s, key.verifying_key()).is_err());
}
#[test]
fn the_headers_carry_the_published_names() {
assert_eq!(HEADER_DIGEST, "X-BDEW-DIGEST");
assert_eq!(HEADER_SIGNATURE, "X-BDEW-SIGNATURE");
assert_eq!(HEADER_CERT, "X-BDEW-CERT");
assert_eq!(cert_header_value(&[1, 2, 3]), ":AQID:");
}
}