scion-sdk-common-types 0.2.0

SCION endhost SDK common types
Documentation
// Copyright 2025 Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Serialize and deserialize valid PEM-encoded Ed25519 signing and verification
//! keys.
//!
//! This module contains serializeable types representing signing (verifying)
//! keys for Ed25519 that serialize to a PKCS8-presentation in the PEM-format.
//!
//! Note: When serializing the signing key, the corresponding public key is
//! omitted. This is such that the output is bit-equivalent with the private key
//! representation generated by open ssl.

use std::{fmt::Display, str::FromStr};

use ed25519_dalek::{
    SigningKey, VerifyingKey,
    pkcs8::{
        self, DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, KeypairBytes,
        spki::der::pem::LineEnding,
    },
};
use jsonwebtoken::{DecodingKey, EncodingKey};
use serde::{Deserialize, Serialize};

/// Ed25519 verifying key in PEM format.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ed25519VerifyingKeyPem(VerifyingKey);

impl Ed25519VerifyingKeyPem {
    /// Returns the corresponding decoding key.
    pub fn to_decoding_key(&self) -> DecodingKey {
        // XXX(dsd): Here, we need to take a detour through pem due to a bug in
        // jsonwebtoken.
        DecodingKey::from_ed_pem(
            self.0
                .to_public_key_pem(line_ending())
                .expect("serialization from a fixed well-defined type")
                .as_bytes(),
        )
        .expect("deserialization from a well-defined serialization")
    }
}

impl Display for Ed25519VerifyingKeyPem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.0
                .to_public_key_pem(line_ending())
                .expect("serialization from a fixed well-defined type")
        )
    }
}

impl From<VerifyingKey> for Ed25519VerifyingKeyPem {
    fn from(value: VerifyingKey) -> Self {
        Self(value)
    }
}

impl Serialize for Ed25519VerifyingKeyPem {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let pem = self
            .0
            .to_public_key_pem(LineEnding::LF)
            .map_err(serde::ser::Error::custom)?;
        serializer.serialize_str(&pem)
    }
}

impl<'e> Deserialize<'e> for Ed25519VerifyingKeyPem {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'e>,
    {
        let pem = String::deserialize(deserializer)?;
        let key = VerifyingKey::from_public_key_pem(&pem).map_err(serde::de::Error::custom)?;
        Ok(Self(key))
    }
}

/// Ed25519 signing key in PEM format.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ed25519SigningKeyPem(SigningKey);

impl Ed25519SigningKeyPem {
    /// Returns the PEM-encoded PKCS8 representation of the signing key.
    //
    // XXX: such methods should not be used freely as we serialize the private
    // key here. I.e., we want to make serialization in logs, etc. as
    // uncomfortable as possible.
    pub fn warning_to_private_key_pem(&self) -> String {
        self.0
            .to_pkcs8_pem(line_ending())
            .expect("serialization from a fixed well-defined type")
            .as_str()
            .into()
    }

    /// Returns the corresponding decoding key.
    pub fn to_decoding_key(&self) -> DecodingKey {
        // XXX(dsd): Here, we need to take a detour through pem due to a bug in
        // jsonwebtoken.
        DecodingKey::from_ed_pem(
            self.0
                .verifying_key()
                .to_public_key_pem(line_ending())
                .expect("serialization from a fixed well-defined type")
                .as_bytes(),
        )
        .expect("deserialization from a well-defined serialization")
    }

    /// Returns the corresponding verifying key.
    pub fn to_verifying_key(&self) -> VerifyingKey {
        self.0.verifying_key()
    }
}

impl From<SigningKey> for Ed25519SigningKeyPem {
    fn from(value: SigningKey) -> Self {
        Self(value)
    }
}

impl FromStr for Ed25519SigningKeyPem {
    type Err = pkcs8::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self(SigningKey::from_pkcs8_pem(s)?))
    }
}

impl Serialize for Ed25519SigningKeyPem {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // Note: We explicitly blank out the public key as:
        // * it is redundant
        // * the private key file provided by openssl does not contain it
        let mut keypair_bytes: KeypairBytes = self.0.clone().into();
        keypair_bytes.public_key = None; // blank out public key
        let pem = keypair_bytes
            .to_pkcs8_pem(line_ending())
            .map_err(serde::ser::Error::custom)?;
        serializer.serialize_str(&pem)
    }
}

impl<'e> Deserialize<'e> for Ed25519SigningKeyPem {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'e>,
    {
        let pem = String::deserialize(deserializer)?;
        let key = SigningKey::from_pkcs8_pem(&pem).map_err(serde::de::Error::custom)?;
        Ok(Self(key))
    }
}

impl From<&Ed25519SigningKeyPem> for EncodingKey {
    fn from(value: &Ed25519SigningKeyPem) -> EncodingKey {
        EncodingKey::from_ed_der(value.0.to_pkcs8_der().expect("should not fail").as_bytes())
    }
}

fn line_ending() -> LineEnding {
    #[cfg(target_family = "windows")]
    let line_ending = LineEnding::CRLF;
    #[cfg(not(target_family = "windows"))]
    let line_ending = LineEnding::LF;

    line_ending
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize, de::DeserializeOwned};

    use crate::ed25519::{Ed25519SigningKeyPem, Ed25519VerifyingKeyPem};

    #[derive(Debug, Serialize, Deserialize)]
    struct ConfigWithVerifyingKey<T> {
        verifying_key: T,
    }

    #[derive(Debug, Serialize, Deserialize)]
    struct ConfigWithSigningKey<T> {
        signing_key: T,
    }

    // $ openssl genpkey -algorithm ed25519 -out ed25519_signing.pem
    const ED25519_SIGNING_KEY_PEM: &str = include_str!("testdata/ed25519_signing.pem");
    // $ openssl pkey -in ed25519_signing.pem -pubout -out ed25519_verifying.pem
    const ED25519_VERIFYING_KEY_PEM: &str = include_str!("testdata/ed25519_verifying.pem");
    // $ openssl genpkey -algorithm RSA -out rsa_private.pem -pkeyopt rsa_keygen_bits:2048
    const RSA_PRI_KEY: &str = include_str!("testdata/rsa_private.pem");
    // $ openssl rsa -pubout -in rsa_private.pem -out rsa_public.pem
    const RSA_PUB_KEY: &str = include_str!("testdata/rsa_public.pem");

    type TestResult = Result<(), Box<dyn std::error::Error>>;

    #[test]
    fn parsing_ed25519_signing_key_succeeds() -> TestResult {
        assert_parse_succeeds::<_, ConfigWithSigningKey<Ed25519SigningKeyPem>>(
            ConfigWithSigningKey {
                signing_key: ED25519_SIGNING_KEY_PEM.to_string(),
            },
        )
    }

    #[test]
    fn parsing_ed25519_verifying_key_succeeds() -> TestResult {
        assert_parse_succeeds::<_, ConfigWithVerifyingKey<Ed25519VerifyingKeyPem>>(
            ConfigWithVerifyingKey {
                verifying_key: ED25519_VERIFYING_KEY_PEM.to_string(),
            },
        )
    }

    #[test]
    fn parsing_ed25519_verifying_key_as_signing_key_fails() -> TestResult {
        assert_parse_fails::<_, ConfigWithVerifyingKey<Ed25519SigningKeyPem>>(
            ConfigWithVerifyingKey {
                verifying_key: ED25519_VERIFYING_KEY_PEM.to_string(),
            },
        )
    }

    #[test]
    fn parsing_ed25519_signing_key_as_verifying_key_fails() -> TestResult {
        assert_parse_fails::<_, ConfigWithSigningKey<Ed25519VerifyingKeyPem>>(
            ConfigWithSigningKey {
                signing_key: ED25519_SIGNING_KEY_PEM.to_string(),
            },
        )
    }

    #[test]
    fn parsing_rsa_public_key_as_ed25519_verifying_key_fails() -> TestResult {
        assert_parse_fails::<_, ConfigWithVerifyingKey<Ed25519VerifyingKeyPem>>(
            ConfigWithVerifyingKey {
                verifying_key: RSA_PUB_KEY.to_string(),
            },
        )
    }

    #[test]
    fn parsing_rsa_private_key_as_ed25519_signing_key_fails() -> TestResult {
        assert_parse_fails::<_, ConfigWithVerifyingKey<Ed25519SigningKeyPem>>(
            ConfigWithSigningKey {
                signing_key: RSA_PRI_KEY.to_string(),
            },
        )
    }

    fn assert_parse_succeeds<T, S>(source_config: T) -> TestResult
    where
        T: Serialize,
        S: Serialize + DeserializeOwned,
    {
        assert_parse_result::<_, S>(source_config, false)
    }

    fn assert_parse_fails<T, S>(source_config: T) -> TestResult
    where
        T: Serialize,
        S: Serialize + DeserializeOwned,
    {
        assert_parse_result::<_, S>(source_config, true)
    }

    fn assert_parse_result<T, S>(source_config: T, expect_parse_failure: bool) -> TestResult
    where
        T: Serialize,
        S: Serialize + DeserializeOwned,
    {
        let expected_serialized = serde_json::to_string(&source_config)?;
        let deserialized = serde_json::from_str::<S>(&expected_serialized);
        let deserialized = if expect_parse_failure {
            assert!(deserialized.is_err());
            return Ok(());
        } else {
            deserialized.unwrap()
        };
        let actual_serialize = serde_json::to_string::<S>(&deserialized)?;
        assert_eq!(strip_cr(expected_serialized), strip_cr(actual_serialize));
        Ok(())
    }

    // Normalize to unix so we have a comparable result in all cases.
    // Note that this leaves room for edge cases that this test will not catch,
    // e.g., when on Unix CRLF is used instead of LF — or the mirror case on
    // windows. However, as we are dealing with plaintext, we allow for this to
    // slip, if in doubt.
    fn strip_cr(s: String) -> String {
        s.replace("\\r\\n", "\\n")
    }
}