use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use crate::exi::{ExiError, ValueCoding};
mod challenge;
pub use challenge::GenChallenge;
#[cfg(feature = "iso2")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso2")))]
pub mod iso2;
#[cfg(feature = "iso20-common")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
pub mod iso20;
pub mod envelope;
pub mod pki;
#[cfg(feature = "pnc-rustcrypto")]
#[cfg_attr(docsrs, doc(cfg(feature = "pnc-rustcrypto")))]
pub mod rustcrypto;
pub const CANONICAL_EXI: &str = "http://www.w3.org/TR/canonical-exi/";
pub const DEFAULT_ID: &str = "ID1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Suite {
EcdsaSha256,
EcdsaSha512,
}
impl Suite {
#[must_use]
pub const fn signature_algorithm(self) -> &'static str {
match self {
Self::EcdsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
Self::EcdsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
}
}
#[must_use]
pub const fn digest_algorithm(self) -> &'static str {
match self {
Self::EcdsaSha256 => "http://www.w3.org/2001/04/xmlenc#sha256",
Self::EcdsaSha512 => "http://www.w3.org/2001/04/xmlenc#sha512",
}
}
#[must_use]
pub const fn digest_len(self) -> usize {
match self {
Self::EcdsaSha256 => 32,
Self::EcdsaSha512 => 64,
}
}
#[must_use]
pub fn from_signature_algorithm(uri: &str) -> Option<Self> {
[Self::EcdsaSha256, Self::EcdsaSha512].into_iter().find(|s| s.signature_algorithm() == uri)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Signed<'a> {
pub id: &'a str,
pub fragment: &'a [u8],
}
impl<'a> Signed<'a> {
#[must_use]
pub const fn new(id: &'a str, fragment: &'a [u8]) -> Self {
Self { id, fragment }
}
}
pub trait Hash {
fn digest(&self, suite: Suite, data: &[u8]) -> Vec<u8>;
}
pub trait Sign {
fn sign(&self, suite: Suite, data: &[u8]) -> Result<Vec<u8>, PncError>;
}
pub trait Verify {
fn verify(&self, suite: Suite, data: &[u8], signature: &[u8]) -> Result<(), PncError>;
}
pub(crate) fn either_coding<T>(
mut attempt: impl FnMut(ValueCoding) -> Result<T, PncError>,
) -> Result<T, PncError> {
match attempt(ValueCoding::Literal) {
Err(first) if matches!(first, PncError::BadSignature | PncError::DigestMismatch { .. }) => {
attempt(ValueCoding::Referenced).map_err(|_| first)
}
other => other,
}
}
#[must_use]
pub fn digests_equal(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
pub const MAX_SIGNED_ELEMENTS: usize = 4;
#[must_use]
pub fn reference_uri(id: &str) -> String {
let mut uri = String::with_capacity(id.len() + 1);
uri.push('#');
uri.push_str(id);
uri
}
pub(crate) fn check_canonicalization(algorithm: &str) -> Result<(), PncError> {
if algorithm == CANONICAL_EXI {
Ok(())
} else {
Err(PncError::BadCanonicalization(String::from(algorithm)))
}
}
pub(crate) fn check_transforms<'a>(
algorithms: Option<impl IntoIterator<Item = &'a str>>,
) -> Result<(), PncError> {
let Some(algorithms) = algorithms else { return Err(PncError::BadTransforms) };
let mut algorithms = algorithms.into_iter();
let Some(only) = algorithms.next() else { return Err(PncError::BadTransforms) };
if algorithms.next().is_some() {
return Err(PncError::BadTransforms);
}
check_canonicalization(only)
}
pub(crate) fn check_suite(
algorithm: &str,
hmac_output_length: Option<i64>,
accepted: &[Suite],
) -> Result<Suite, PncError> {
let suite = Suite::from_signature_algorithm(algorithm)
.filter(|s| accepted.contains(s))
.ok_or_else(|| PncError::UnsupportedAlgorithm(String::from(algorithm)))?;
if hmac_output_length.is_some() {
return Err(PncError::UnsupportedAlgorithm(String::from(algorithm)));
}
Ok(suite)
}
pub(crate) fn check_forbidden<'a>(
signed_info_id: Option<&str>,
signature_value_id: Option<&str>,
reference_types: impl IntoIterator<Item = Option<&'a str>>,
) -> Result<(), PncError> {
if signed_info_id.is_some() {
return Err(PncError::ForbiddenField { field: "SignedInfo/@Id" });
}
if signature_value_id.is_some() {
return Err(PncError::ForbiddenField { field: "SignatureValue/@Id" });
}
for r#type in reference_types {
if r#type.is_some() {
return Err(PncError::ForbiddenField { field: "Reference/@Type" });
}
}
Ok(())
}
pub(crate) fn check_reference(
suite: Suite,
digest_algorithm: &str,
digest_value: &[u8],
element: &Signed<'_>,
hash: &impl Hash,
) -> Result<(), PncError> {
if digest_algorithm != suite.digest_algorithm() {
return Err(PncError::UnsupportedAlgorithm(String::from(digest_algorithm)));
}
let expected = hash.digest(suite, element.fragment);
if digests_equal(&expected, digest_value) {
Ok(())
} else {
Err(PncError::DigestMismatch { id: element.id.into() })
}
}
pub(crate) fn check_session(
signed: &[u8],
session: crate::session::SessionId,
) -> Result<(), PncError> {
match crate::session::SessionId::from_slice(signed) {
Ok(id) if id == session => Ok(()),
_ => Err(PncError::SessionMismatch),
}
}
pub(crate) fn check_echo<T: PartialEq>(
echoed: &T,
issued: &T,
field: &'static str,
) -> Result<(), PncError> {
if echoed == issued { Ok(()) } else { Err(PncError::NotAsIssued { field }) }
}
pub(crate) fn pair<'a, R>(
references: &'a [R],
uri_of: impl Fn(&'a R) -> Option<&'a str>,
elements: &'a [Signed<'a>],
) -> Result<Vec<(&'a R, &'a Signed<'a>)>, PncError> {
if references.len() > MAX_SIGNED_ELEMENTS {
return Err(PncError::TooManyReferences { references: references.len() });
}
if references.len() != elements.len() {
return Err(PncError::ReferenceMismatch {
references: references.len(),
elements: elements.len(),
});
}
let mut used = alloc::vec![false; elements.len()];
let mut paired = Vec::with_capacity(references.len());
for reference in references {
let uri = uri_of(reference).ok_or(PncError::MissingUri)?;
let id = uri.strip_prefix('#').ok_or(PncError::MissingUri)?;
let index = elements
.iter()
.enumerate()
.find(|&(i, e)| e.id == id && !used[i])
.map(|(i, _)| i)
.ok_or(PncError::UnknownReference)?;
used[index] = true;
paired.push((reference, &elements[index]));
}
Ok(paired)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PncError {
Exi(ExiError),
UnsupportedAlgorithm(String),
BadCanonicalization(String),
MissingUri,
UnknownReference,
ReferenceMismatch {
references: usize,
elements: usize,
},
BadTransforms,
TooManyReferences {
references: usize,
},
ForbiddenField {
field: &'static str,
},
DigestMismatch {
id: String,
},
BadSignature,
BadEnvelope {
len: usize,
},
KeyMismatch,
BadChallengeLength {
len: usize,
},
ChallengeMismatch,
MissingId,
MissingChallenge,
SessionMismatch,
NotAsIssued {
field: &'static str,
},
Backend(&'static str),
}
impl From<ExiError> for PncError {
fn from(e: ExiError) -> Self {
Self::Exi(e)
}
}
impl fmt::Display for PncError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exi(e) => write!(f, "EXI: {e}"),
Self::UnsupportedAlgorithm(uri) => {
write!(f, "algorithm {uri} is outside the ISO 15118 profile")
}
Self::BadCanonicalization(uri) => {
write!(f, "canonicalization {uri} is not {CANONICAL_EXI}")
}
Self::MissingUri => f.write_str("a Reference has no same-document URI"),
Self::UnknownReference => {
f.write_str("a Reference names an element that was not given")
}
Self::ReferenceMismatch { references, elements } => write!(
f,
"the signature covers {references} elements, {elements} were given to check"
),
Self::BadTransforms => {
f.write_str("Transforms must be exactly one canonical-EXI transform")
}
Self::BadEnvelope { len } => {
write!(f, "{len} is not a length ISO 15118 gives that field")
}
Self::KeyMismatch => f.write_str(
"the delivered private key does not belong to the certificate it came with",
),
Self::TooManyReferences { references } => write!(
f,
"the signature covers {references} elements; ISO 15118 allows at most \
{MAX_SIGNED_ELEMENTS}"
),
Self::ForbiddenField { field } => {
write!(f, "{field} is not permitted in a V2G signature")
}
Self::DigestMismatch { id } => write!(f, "the digest of {id} does not match"),
Self::BadSignature => f.write_str("the signature over SignedInfo does not verify"),
Self::BadChallengeLength { len } => {
write!(f, "GenChallenge is {len} bytes, the schema requires {}", GenChallenge::LEN)
}
Self::ChallengeMismatch => {
f.write_str("the signed GenChallenge is not the one this session issued")
}
Self::MissingId => f.write_str("the signed element has no Id to reference"),
Self::MissingChallenge => {
f.write_str("a Plug & Charge AuthorizationReq carried no GenChallenge")
}
Self::SessionMismatch => f.write_str("the signed element names a different session"),
Self::NotAsIssued { field } => {
write!(f, "the signed {field} is not the one this side issued")
}
Self::Backend(what) => write!(f, "crypto backend: {what}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for PncError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Exi(e) => Some(e),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_comparison_is_length_safe() {
assert!(digests_equal(&[1, 2, 3], &[1, 2, 3]));
assert!(!digests_equal(&[1, 2, 3], &[1, 2, 4]));
assert!(!digests_equal(&[1, 2, 3], &[1, 2]));
assert!(digests_equal(&[], &[]));
}
#[test]
fn suites_round_trip_through_their_uris() {
for suite in [Suite::EcdsaSha256, Suite::EcdsaSha512] {
assert_eq!(Suite::from_signature_algorithm(suite.signature_algorithm()), Some(suite));
}
assert_eq!(Suite::from_signature_algorithm("http://example/rsa"), None);
assert_ne!(Suite::EcdsaSha256.digest_algorithm(), Suite::EcdsaSha512.digest_algorithm());
}
#[test]
fn a_reference_uri_is_a_same_document_pointer() {
assert_eq!(reference_uri("ID1"), "#ID1");
}
}