diff --git a/Cargo.lock b/Cargo.lock index 80a5067..06855fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,6 +765,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -1509,6 +1510,7 @@ dependencies = [ "clap", "dialoguer", "dirs", + "ed25519-dalek", "futures", "hickory-proto", "hickory-resolver", @@ -1523,16 +1525,15 @@ dependencies = [ "itertools", "libc", "local-ip-address", - "p256", "pest", "pest_derive", "pkcs8", "prost", "protoc-bin-vendored", "rand 0.9.2", + "rand_core 0.6.4", "rayon", "regex", - "ring", "rustls", "rustls-rustcrypto", "rustls-webpki 0.103.8", diff --git a/Cargo.toml b/Cargo.toml index ffe7a9a..1e92c38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,12 +51,12 @@ tower = { version = "0.5.2", features = ["util", "timeout", "load-shed", "limit" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } yaml-rust2 = "0.10.3" -p256 = "0.13.2" +ed25519-dalek = { version = "2.2.0", features = ["pkcs8", "pem", "rand_core"] } pest = "2.7" pest_derive = "2.7" pkcs8 = "0.10.2" +rand_core = { version = "0.6", features = ["getrandom"] } regex = "1.11.1" -ring = "0.17" rustls = { version = "0.23.31", default-features = false } thiserror = "2.0.16" x509-cert = { version = "0.2.5", features = ["builder"] } diff --git a/context/interfaces/src/imid.md b/context/interfaces/src/imid.md index 6f28b88..151d8f2 100644 --- a/context/interfaces/src/imid.md +++ b/context/interfaces/src/imid.md @@ -14,27 +14,24 @@ pub struct Imid { pub string: String, /// Public key used to verify signatures from this identity. - pub public_key: PublicKey, + pub public_key: VerifyingKey, } /// Private key material used to sign Intermesh messages. pub struct ImidKeypair { /// Secret signing key. - pub private_key: SecretKey, + pub private_key: SigningKey, } impl Imid { - /// Create an object suitable for signature verification. - pub fn to_unparsed_public_key(&self) -> UnparsedPublicKey>; - /// Verify that `signature` was produced by this identity for `message`. pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), _>; /// Parse an IMID from its canonical string form. pub fn from_string(s: String) -> anyhow::Result; - /// Create an IMID from a P-256 public key. - pub fn from_public_key(public_key: PublicKey) -> Self; + /// Create an IMID from an Ed25519 public key. + pub fn from_public_key(public_key: VerifyingKey) -> Self; /// Convert to `.imid` DNS-name form. pub fn to_dns_name(&self) -> String; @@ -45,7 +42,7 @@ impl Imid { /// Convert to a rustls server name for TLS verification. pub fn to_server_name(&self) -> ServerName<'static>; - /// Parse from compressed SEC1 public key bytes. + /// Parse from raw Ed25519 public key bytes. pub fn from_bytes(bytes: &[u8]) -> anyhow::Result; /// Derive an IMID from a TLS certificate without validating it. @@ -62,8 +59,8 @@ impl ImidKeypair { /// Serialize the private key as PEM bytes. pub fn to_pem(&self) -> Vec; - /// Serialize the private key as PKCS#8 EC DER bytes. - pub fn to_ec_der(&self) -> Vec; + /// Serialize the private key as PKCS#8 DER bytes. + pub fn to_pkcs8_der(&self) -> Vec; /// Return the public identity for this keypair. pub fn to_imid(&self) -> Imid; diff --git a/proto/intermesh.proto b/proto/intermesh.proto index b5eb1d5..e0eabe0 100644 --- a/proto/intermesh.proto +++ b/proto/intermesh.proto @@ -126,7 +126,7 @@ message Authz { // Endorsement message transmitted over the network message Endorsement { bytes payload = 1; // Serialized `EndorsementData` (binary protobuf) - bytes signature = 2; // ECDSA P-256 signature over data field + bytes signature = 2; // Ed25519 signature over data field } message GossipUpdate { @@ -137,7 +137,7 @@ message GossipUpdate { // Signed revocation message message Revocation { bytes payload = 1; // Serialized `Endorsement` (binary protobuf) - bytes signature = 2; // ECDSA P-256 signature over payload + bytes signature = 2; // Ed25519 signature over payload } // Admin service for local CLI commands only over the unix socket. These @@ -191,4 +191,3 @@ message EndorseRequest { message EndorseResponse {} - diff --git a/src/connect.rs b/src/connect.rs index e944469..24fc330 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -51,7 +51,7 @@ impl IntermeshClient { .with_custom_certificate_verifier(verifier) .with_client_auth_cert( vec![keypair.gen_cert()], - PrivateKeyDer::try_from(keypair.to_ec_der()).assert(), + PrivateKeyDer::try_from(keypair.to_pkcs8_der()).assert(), ) .assert(); @@ -188,7 +188,7 @@ pub(crate) fn intermesh_server_stream( listener: TcpListener, ) -> impl Stream> { let cert_der = keypair.gen_cert(); - let private_key_der = keypair.to_ec_der(); + let private_key_der = keypair.to_pkcs8_der(); // TODO(Issue/64): Evaluate whether RustCrypto is an appropriate default let mut server_config = @@ -589,7 +589,7 @@ mod tests { let server_verifier = Arc::new(IntermeshVerifier::new_permissive()); let cert_der = server_keypair.gen_cert(); - let private_key_der = server_keypair.to_ec_der(); + let private_key_der = server_keypair.to_pkcs8_der(); let mut server_config = ServerConfig::builder_with_provider(Arc::new(rustls_rustcrypto::provider())) diff --git a/src/imid.rs b/src/imid.rs index 66ce61e..e1bf893 100644 --- a/src/imid.rs +++ b/src/imid.rs @@ -5,41 +5,35 @@ //! An IMID is a compact, self-contained cryptographic identifier consisting of: //! //! ```text -//! [base64url-encoded-public-key:44][version:1] +//! [prefix:A][base64url-encoded-public-key:43][version:2] //! ``` //! //! - **Total length**: 45 characters +//! - **Prefix character**: 'A' //! - **Encoding**: Base64url (URL-safe, no padding) per RFC 4648 -//! - **Key format**: P-256 compressed public key (33 bytes → 44 base64url chars) -//! - **Version character**: Currently '1' for P-256 ECDSA keys (at end) +//! - **Key format**: Ed25519 public key (32 bytes → 43 base64url chars) +//! - **Version character**: Currently '2' for Ed25519 keys (at end) //! -//! The version is placed at the end (rather than the beginning) to ensure DNS -//! compatibility. Base64url encoding can produce strings ending with `-` or `_`, -//! which are invalid for DNS labels. By placing the version character at the -//! end, IMIDs are guaranteed to end with a digit, making them valid DNS names. +//! Base64url encoding can produce strings beginning or ending with `-`, which +//! is invalid for DNS labels. The fixed prefix and trailing version ensure that +//! IMIDs begin and end with alphanumeric characters. //! //! ## Example //! //! ```text -//! AkR3kVnR2c8HpzNQ7xJL4m2vB9FqW1xyT5PnM7R3kVnZ1 -//! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ base64url public key -//! ^ version (P-256) +//! AAkR3kVnR2c8HpzNQ7xJL4m2vB9FqW1xyT5PnM7R3kVn2 +//! ^ prefix +//! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ base64url public key +//! ^ version (Ed25519) //! ``` use crate::assert::UnwrapAssert; use anyhow::{bail, Context, Result}; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; -use p256::ecdsa::{DerSignature, SigningKey}; -use p256::elliptic_curve::rand_core::{OsRng, RngCore}; -use p256::elliptic_curve::sec1::ToEncodedPoint; -use p256::{PublicKey, SecretKey}; +use ed25519_dalek::{Signature, SignatureError, Signer, SigningKey, Verifier, VerifyingKey}; use pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding}; -use ring::error::Unspecified; -use ring::rand::SystemRandom; -use ring::signature::{ - EcdsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_FIXED, ECDSA_P256_SHA256_FIXED_SIGNING, -}; +use rand_core::{OsRng, RngCore}; use rustls::pki_types::{CertificateDer, ServerName}; use rustls::CertificateError::BadEncoding; use rustls::Error as TlsError; @@ -51,31 +45,49 @@ use std::str; use std::str::FromStr; use std::time::Duration; use x509_cert::builder::{Builder, CertificateBuilder, Profile}; +use x509_cert::der::asn1::BitString; +use x509_cert::der::referenced::OwnedToRef; use x509_cert::der::{Decode, Encode}; use x509_cert::name::Name; use x509_cert::serial_number::SerialNumber; -use x509_cert::spki::SubjectPublicKeyInfoOwned; +use x509_cert::spki::{SignatureBitStringEncoding, SubjectPublicKeyInfoOwned}; use x509_cert::time::Validity; use x509_cert::Certificate; // IMID validation constants -const IMID_VERSION_P256: char = '1'; -const IMID_LENGTH: usize = 45; // 1 version char + 44 base64url chars +const IMID_PREFIX: char = 'A'; +const IMID_VERSION_ED25519: char = '2'; +const IMID_LENGTH: usize = 45; // 1 prefix char + 43 base64url chars + 1 version char +const ED25519_PUBLIC_KEY_LENGTH: usize = 32; // Certificate expiration time const CERT_VALIDITY_DURATION: Duration = Duration::from_hours(24); +struct X509Ed25519Signature(Signature); + +impl SignatureBitStringEncoding for X509Ed25519Signature { + fn to_bitstring(&self) -> x509_cert::der::Result { + BitString::new(0, self.0.to_bytes()) + } +} + +impl Signer for SigningKey { + fn try_sign(&self, msg: &[u8]) -> std::result::Result { + Ok(X509Ed25519Signature(Signer::::sign(self, msg))) + } +} + /// A public Intermesh ID - the network identifier for a peer #[derive(Clone, SerializeDisplay, DeserializeFromStr)] pub struct Imid { pub string: String, - pub public_key: PublicKey, + pub public_key: VerifyingKey, } /// A private keypair that can generate IMIDs and sign messages #[derive(Debug, Clone)] pub struct ImidKeypair { - pub private_key: SecretKey, + pub private_key: SigningKey, imid: Imid, } @@ -88,16 +100,10 @@ impl FromStr for Imid { } impl Imid { - /// Create an `UnparsedPublicKey` for signature verification - #[must_use] - pub fn to_unparsed_public_key(&self) -> UnparsedPublicKey> { - let public_key_der = self.public_key.to_encoded_point(false).as_bytes().to_vec(); - UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, public_key_der) - } - /// Verify a signature over data using this IMID's public key - pub fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), Unspecified> { - self.to_unparsed_public_key().verify(data, signature) + pub fn verify(&self, data: &[u8], signature: &[u8]) -> std::result::Result<(), SignatureError> { + let signature = Signature::from_slice(signature)?; + self.public_key.verify(data, &signature) } /// Parse an IMID string @@ -107,20 +113,22 @@ impl Imid { bail!("invalid format"); } + if !s.starts_with(IMID_PREFIX) { + bail!("invalid format"); + } + let version = s.chars().last().assert(); - if version != IMID_VERSION_P256 { + if version != IMID_VERSION_ED25519 { bail!("unsupported version"); } - let b64_part = &s[..s.len() - 1]; + let b64_part = &s[IMID_PREFIX.len_utf8()..s.len() - 1]; let key_bytes = URL_SAFE_NO_PAD .decode(b64_part) .ok() .context("base64 decode error")?; - let public_key = PublicKey::from_sec1_bytes(&key_bytes) - .ok() - .context("invalid key")?; + let public_key = Self::public_key_from_bytes(&key_bytes)?; Ok(Imid { string: s, @@ -128,16 +136,12 @@ impl Imid { }) } - /// Create an IMID from a P-256 public key + /// Create an IMID from an Ed25519 public key #[must_use] - pub fn from_public_key(public_key: PublicKey) -> Self { - // Convert the public key to SEC1 compressed point format (33 bytes) - let compressed_key = public_key.to_encoded_point(true); - let key_bytes = compressed_key.as_bytes(); - - // Base64url encode (no padding) - 33 bytes -> 44 chars - let b64 = URL_SAFE_NO_PAD.encode(key_bytes); - let string = format!("{b64}{IMID_VERSION_P256}"); + pub fn from_public_key(public_key: VerifyingKey) -> Self { + // Base64url encode (no padding) - 32 bytes -> 43 chars. + let encoded = URL_SAFE_NO_PAD.encode(public_key.as_bytes()); + let string = format!("{IMID_PREFIX}{encoded}{IMID_VERSION_ED25519}"); assert_eq!(string.len(), IMID_LENGTH); @@ -162,14 +166,17 @@ impl Imid { ServerName::try_from(self.to_dns_name()).assert() } - /// Create IMID from raw bytes (33-byte SEC1 compressed public key) + /// Create IMID from raw bytes (32-byte Ed25519 public key) pub fn from_bytes(bytes: &[u8]) -> Result { - let public_key = PublicKey::from_sec1_bytes(bytes) - .ok() - .context("invalid key")?; + let public_key = Self::public_key_from_bytes(bytes)?; Ok(Self::from_public_key(public_key)) } + fn public_key_from_bytes(bytes: &[u8]) -> Result { + let bytes: [u8; ED25519_PUBLIC_KEY_LENGTH] = bytes.try_into().context("invalid key")?; + VerifyingKey::from_bytes(&bytes).context("invalid key") + } + /// Derive an IMID from the public key embedded in a TLS certificate. /// /// This does not validate the certificate; callers should only use it if @@ -178,14 +185,12 @@ impl Imid { let parsed = Certificate::from_der(cert).map_err(|_| TlsError::InvalidCertificate(BadEncoding))?; - let public_key_bytes = parsed + let public_key_info = parsed .tbs_certificate .subject_public_key_info - .subject_public_key - .as_bytes() - .ok_or_else(|| TlsError::InvalidCertificate(BadEncoding))?; + .owned_to_ref(); - let public_key = PublicKey::from_sec1_bytes(public_key_bytes) + let public_key = VerifyingKey::try_from(public_key_info) .map_err(|_| TlsError::InvalidCertificate(BadEncoding))?; Ok(Imid::from_public_key(public_key)) @@ -245,22 +250,25 @@ impl Hash for Imid { } impl ImidKeypair { - fn from_private_key(private_key: SecretKey) -> Self { - let imid = Imid::from_public_key(private_key.public_key()); + fn from_private_key(private_key: SigningKey) -> Self { + let imid = Imid::from_public_key(private_key.verifying_key()); Self { private_key, imid } } /// Generate a new random keypair pub fn generate() -> Self { - Self::from_private_key(SecretKey::random(&mut OsRng)) + loop { + let keypair = Self::from_private_key(SigningKey::generate(&mut OsRng)); + if ServerName::try_from(keypair.imid.to_dns_name()).is_ok() { + return keypair; + } + } } /// Load a keypair from PEM-encoded private key bytes pub fn from_pem(pem_bytes: &[u8]) -> Result { let pem_str = str::from_utf8(pem_bytes).ok().context("invalid key")?; - let private_key = SecretKey::from_pkcs8_pem(pem_str) - .ok() - .context("invalid key")?; + let private_key = SigningKey::from_pkcs8_pem(pem_str).context("invalid key")?; Ok(Self::from_private_key(private_key)) } @@ -273,9 +281,9 @@ impl ImidKeypair { .to_vec() } - /// Convert keypair to DER-encoded EC private key bytes (PKCS#8 format for jsonwebtoken) + /// Convert keypair to DER-encoded private key bytes (PKCS#8 format) #[must_use] - pub fn to_ec_der(&self) -> Vec { + pub fn to_pkcs8_der(&self) -> Vec { self.private_key.to_pkcs8_der().assert().as_bytes().to_vec() } @@ -288,11 +296,9 @@ impl ImidKeypair { /// Sign data with this keypair #[must_use] pub fn sign(&self, data: &[u8]) -> Vec { - let rng = SystemRandom::new(); - let signing_key = - EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &self.to_ec_der(), &rng) - .assert(); - signing_key.sign(&rng, data).assert().as_ref().to_vec() + Signer::::sign(&self.private_key, data) + .to_bytes() + .to_vec() } #[must_use] @@ -300,8 +306,7 @@ impl ImidKeypair { // Common name set to "Intermesh-ID" so users do not think that the common name // communicates who(imid) they are messaging with let subject = Name::from_str("CN=intermesh-id").assert(); - let signing_key_der = self.private_key.to_pkcs8_der().assert(); - let signing_key = SigningKey::from_pkcs8_der(signing_key_der.as_bytes()).assert(); + let signing_key = &self.private_key; // Generate random serial number (20 bytes per RFC 5280 4.1.2.2 max) let mut serial_bytes = [0u8; 20]; @@ -314,15 +319,15 @@ impl ImidKeypair { let validity = Validity::from_now(CERT_VALIDITY_DURATION).assert(); // Use Profile::Leaf for key usage: digital signature (for signing - // endorsements) and key agreement (RFC 5280 4.2.1.3) + // endorsements). Ed25519 does not support key agreement. let profile = Profile::Leaf { issuer: subject.clone(), - enable_key_agreement: true, + enable_key_agreement: false, enable_key_encipherment: false, }; let public_key = - SubjectPublicKeyInfoOwned::from_key(self.private_key.public_key()).assert(); + SubjectPublicKeyInfoOwned::from_key(self.private_key.verifying_key()).assert(); let cert = CertificateBuilder::new( profile, @@ -330,10 +335,10 @@ impl ImidKeypair { validity, subject, public_key, - &signing_key, + signing_key, ) .assert() - .build::() + .build::() .assert() .to_der() .assert(); @@ -350,21 +355,11 @@ impl ImidKeypair { /// embedded in expected output. #[must_use] pub fn test_keypair(label: &str) -> Self { - use p256::FieldBytes; use sha2::{Digest, Sha256}; - // A SHA-256 digest is 32 bytes, which matches the scalar size for P-256. - // Not every 32-byte string is a valid scalar, so retry by re-hashing the - // digest bytes deterministically until we get a valid scalar. - let mut bytes: [u8; 32] = Sha256::digest(label.as_bytes()).into(); - loop { - let field_bytes = FieldBytes::from(bytes); - if let Ok(private_key) = SecretKey::from_bytes(&field_bytes) { - return Self::from_private_key(private_key); - } - - bytes = Sha256::digest(bytes).into(); - } + // Ed25519 signing keys are 32-byte seeds, matching SHA-256 output. + let bytes: [u8; 32] = Sha256::digest(label.as_bytes()).into(); + Self::from_private_key(SigningKey::from_bytes(&bytes)) } } @@ -381,7 +376,8 @@ mod tests { // IMID should have correct format assert_eq!(imid.string.len(), IMID_LENGTH); - assert!(imid.string.ends_with(IMID_VERSION_P256)); + assert!(imid.string.starts_with(IMID_PREFIX)); + assert!(imid.string.ends_with(IMID_VERSION_ED25519)); // Verify we can parse the generated key by round-tripping let parsed_imid = Imid::from_string(imid.string.clone()).assert(); @@ -427,25 +423,28 @@ mod tests { let err = Imid::from_string("1".to_string()).unwrap_err(); assert!(err.to_string().contains("invalid format")); - // Just version "2" - should fail (unsupported version AND wrong length) + // Just version "2" - should fail due to wrong length let err = Imid::from_string("2".to_string()).unwrap_err(); assert!(err.to_string().contains("invalid format")); - // Generate a valid IMID and change version to "2" + // Generate a valid IMID and change version to the old P-256 version "1" let keypair = ImidKeypair::generate(); let valid_imid = keypair.to_imid(); let mut chars: Vec = valid_imid.string.chars().collect(); let last_idx = chars.len() - 1; - chars[last_idx] = '2'; + chars[last_idx] = '1'; let invalid_version_imid: String = chars.into_iter().collect(); let err = Imid::from_string(invalid_version_imid).unwrap_err(); assert!(err.to_string().contains("unsupported version")); - // Valid base64 that decodes to an invalid key (all zeros) - let padded = format!("{}1", "A".repeat(IMID_LENGTH - 1)); - let err = Imid::from_string(padded).unwrap_err(); - assert!(err.to_string().contains("invalid key")); + let mut invalid_prefix_imid = valid_imid.string; + invalid_prefix_imid.replace_range(..1, "B"); + let err = Imid::from_string(invalid_prefix_imid).unwrap_err(); + assert!(err.to_string().contains("invalid format")); + + let err = Imid::from_string(format!("A{}2", "!".repeat(IMID_LENGTH - 2))).unwrap_err(); + assert!(err.to_string().contains("base64 decode error")); } #[test] @@ -481,10 +480,10 @@ mod tests { #[test] fn test_der_conversion() { let keypair1 = ImidKeypair::generate(); - let der_bytes = keypair1.to_ec_der(); + let der_bytes = keypair1.to_pkcs8_der(); - // Verify the DER represents the same key by round-tripping through SecretKey - let private_key = SecretKey::from_pkcs8_der(&der_bytes).assert(); + // Verify the DER represents the same key by round-tripping through SigningKey + let private_key = SigningKey::from_pkcs8_der(&der_bytes).assert(); let keypair2 = ImidKeypair::from_private_key(private_key); let imid1 = keypair1.to_imid(); @@ -493,36 +492,18 @@ mod tests { assert_eq!(imid1, imid2); } - #[test] - fn test_unparsed_public_key_creation() { - let keypair = ImidKeypair::generate(); - let imid = keypair.to_imid(); - - // Test that we can create an UnparsedPublicKey for verification - let unparsed_key = imid.to_unparsed_public_key(); - - // Test that it can be used for verification by attempting to verify dummy data - // This should fail (wrong signature) but validates the key format is correct - let result = unparsed_key.verify(b"test message", &[0u8; 64]); - assert!(result.is_err()); // Expected to fail due to invalid signature - } - #[test] fn test_cert_signature_verification_with_imid_public_key() { - use p256::ecdsa::signature::Verifier; - use p256::ecdsa::{Signature, VerifyingKey}; - let keypair = ImidKeypair::generate(); let imid = keypair.to_imid(); let cert_der = keypair.gen_cert(); let cert = Certificate::from_der(cert_der.as_ref()).assert(); - let verifying_key = VerifyingKey::from(imid.public_key); let tbs_cert_der = cert.tbs_certificate.to_der().assert(); - let signature = Signature::from_der(cert.signature.as_bytes().assert()).assert(); + let signature = Signature::from_slice(cert.signature.as_bytes().assert()).assert(); assert!( - verifying_key.verify(&tbs_cert_der, &signature).is_ok(), + imid.public_key.verify(&tbs_cert_der, &signature).is_ok(), "Certificate signature verification with IMID public key failed" ); } @@ -587,9 +568,9 @@ mod tests { let keypair = ImidKeypair::generate(); let imid = keypair.to_imid(); - // 33-byte SEC1 compressed public key, the wire form `from_bytes` parses. - let bytes = imid.public_key.to_encoded_point(true).as_bytes().to_vec(); - assert_eq!(bytes.len(), 33, "compressed SEC1 key should be 33 bytes"); + // 32-byte Ed25519 public key, the wire form `from_bytes` parses. + let bytes = imid.public_key.to_bytes(); + assert_eq!(bytes.len(), 32, "ed25519 public key should be 32 bytes"); let recovered_imid = Imid::from_bytes(&bytes).assert(); assert_eq!(imid, recovered_imid); @@ -601,12 +582,12 @@ mod tests { assert!(Imid::from_bytes(&[]).is_err()); // Wrong length (too short) - assert!(Imid::from_bytes(&[0u8; 32]).is_err()); + assert!(Imid::from_bytes(&[0u8; 31]).is_err()); // Wrong length (too long) - assert!(Imid::from_bytes(&[0u8; 34]).is_err()); - - // Invalid key (all zeros) assert!(Imid::from_bytes(&[0u8; 33]).is_err()); + + // Ed25519 compressed public keys do not have a cheap invalid byte pattern; + // malformed wire keys are rejected by length or certificate SPKI parsing. } } diff --git a/src/modules/adhoc/cli.rs b/src/modules/adhoc/cli.rs index 95fd40b..2f7de4b 100644 --- a/src/modules/adhoc/cli.rs +++ b/src/modules/adhoc/cli.rs @@ -367,9 +367,9 @@ mod tests { let out = render_mesh_status(&my_imid, Some("db.test.mesh"), Some(&root_imid)); let expected = r" -My IMID: AsXgYLhIrH2Y2_0O-TaY2Ng1BPXKDRF2yJX8wiRAPrR71 +My IMID: ApM6QPFlSufAaeo-UcEah-AMDuVmKW89L9MgmbBWpgCM2 My Name: db.test.mesh -Root IMID: A5iLdSS6EI5gHm14NqbBeaprrpBehMFKMn1ObhVeevHo1 +Root IMID: AHnnKX6ZuWpGRuFvFKHc-WHnOakP2WYBOKo2fjW4TY9w2 "; @@ -377,7 +377,7 @@ Root IMID: A5iLdSS6EI5gHm14NqbBeaprrpBehMFKMn1ObhVeevHo1 // When no mesh membership, only show self IMID. let out = render_mesh_status(&my_imid, None, None); - let expected = "My IMID: AsXgYLhIrH2Y2_0O-TaY2Ng1BPXKDRF2yJX8wiRAPrR71\n\n"; + let expected = "My IMID: ApM6QPFlSufAaeo-UcEah-AMDuVmKW89L9MgmbBWpgCM2\n\n"; assert_eq!(out, expected); } @@ -420,11 +420,11 @@ ccccccccccccccccccccccccccdddddddddddddddddddddddddddd ); let expected = r" Bootstrap token from: - IMID: Ahn52S8k85DMXYAM-QP6NBj5nDoemmNXpy-Wex2mPBje1 + IMID: AJSpDms83OG-RBy9tjJC8zR-_1iNYSPhlcJf9qS-_Zl82 IP: 192.0.2.10 Join test.mesh as db.test.mesh? -Joining trusts Ahn52S8k85DMXYAM... as the mesh root, allowing it to: +Joining trusts AJSpDms83OG-RBy9... as the mesh root, allowing it to: - Assign names under **.test.mesh - Assign constraints (trust rules) diff --git a/src/state.rs b/src/state.rs index d50656b..04e7a94 100644 --- a/src/state.rs +++ b/src/state.rs @@ -33,7 +33,7 @@ mod messages; pub(crate) use messages::Update as MessageUpdate; -const STATE_FILE_VERSION: u32 = 1; +const STATE_FILE_VERSION: u32 = 2; /// CLI arguments for the daemon command. /// Uses clap's env feature for automatic env var fallback. diff --git a/src/state/messages.rs b/src/state/messages.rs index d6d977d..ba8dad2 100644 --- a/src/state/messages.rs +++ b/src/state/messages.rs @@ -468,9 +468,9 @@ mod tests { #[test] fn removing_latest_uses_deterministic_promotion_order() { // After removing the current latest, the next latest is the retained - // signing with the largest (expires, issued, signature_bytes). The - // signature byte tie-breaker only matters when expires and issued - // both tie. + // signing with the largest (expires, issued). Ed25519 signatures are + // deterministic, so identical payloads cannot produce distinct retained + // signings that only differ by signature bytes. let mut fix = TestFixture::new(); let store = store_with_imid(&mut fix, "charlie"); let base = TestEndorsement { @@ -480,31 +480,19 @@ mod tests { ..Default::default() }; let older = base.clone().sign_with_timestamps(&mut fix, 100, 200); - let tie_a = base.clone().sign_with_timestamps(&mut fix, 200, 300); - let tie_b = base.clone().sign_with_timestamps(&mut fix, 200, 300); + let tie = base.clone().sign_with_timestamps(&mut fix, 200, 300); let newest = base.sign_with_timestamps(&mut fix, 100, 400); - assert_ne!(tie_a, tie_b); - let expected = if tie_a.signature_bytes() > tie_b.signature_bytes() { - tie_a.clone() - } else { - tie_b.clone() - }; - - add( - &store, - vec![newest.clone(), tie_a.clone(), tie_b.clone(), older.clone()], - ); + add(&store, vec![newest.clone(), tie.clone(), older.clone()]); assert_eq!(store.messages().0, BTreeSet::from([newest.clone()])); remove(&store, vec![newest]); - assert_eq!(store.messages().0, BTreeSet::from([expected])); + assert_eq!(store.messages().0, BTreeSet::from([tie.clone()])); let retained = store.messages_with_retained().0; - assert_eq!(retained.len(), 3); + assert_eq!(retained.len(), 2); assert!(retained.contains(&older)); - assert!(retained.contains(&tie_a)); - assert!(retained.contains(&tie_b)); + assert!(retained.contains(&tie)); } #[test] @@ -525,16 +513,7 @@ mod tests { let first = base.clone().sign_with_timestamps(&mut fix, 200, 300); let second = base.sign_with_timestamps(&mut fix, 200, 300); - assert_ne!(first, second); - let expected = if first.signature_bytes() > second.signature_bytes() { - first.clone() - } else { - second.clone() - }; - - let store = Harness::new(fix.imid("alice")); - add(&store, vec![first, second]); - assert_eq!(store.messages().0, BTreeSet::from([expected])); + assert_eq!(first, second); } #[test] diff --git a/src/state_dump.rs b/src/state_dump.rs index cd6eae8..7dffb16 100644 --- a/src/state_dump.rs +++ b/src/state_dump.rs @@ -748,34 +748,34 @@ mod tests { let expected = r" Config -Self: Aj1Lizc7Xdv-DjFv6wlk9-ocRmzqejaHz-9H27ngqcXf1 +Self: AyRxMmLfKg1rSbhpX8BtfnF-CEwBwhEPe0cmT2Nb5rqg2 Admin socket: /run/intermesh/admin.sock Log file: /var/log/intermesh.log Adhoc Mesh: test.mesh Role: member -Root: A3L-mN23j3FJOvXZkXt45fsAMxeRa1RN4Tgd-m0_9Xp81 +Root: AF4BvPyD8vGkQ1ueSxfJ9ff_qTxJSr9-lJ9dCqGjth4M2 Name: me.test.mesh Derivation 3 Names -db.test.mesh → A1UqiwUlUpEHC0b_xtjSRHollrXSPmkXNasb40dX9xVi1 -web.test.mesh → AoHQqnbGQu_8OBYXtpQQLvyrA3Mq7FDa-P5JRLUasSDL1 +db.test.mesh → AYxQdnOD7mLlbsG8l-tf03mVuBHrpq6re-XcrTv1A0n82 +web.test.mesh → AMc61fS0kzlB5k8cReTbjBbK1FrWt12hvzPYwnMyF2vQ2 IPs -A1UqiwUlUpEHC0b_xtjSRHollrXSPmkXNasb40dX9xVi1 → 10.0.0.2 -AoHQqnbGQu_8OBYXtpQQLvyrA3Mq7FDa-P5JRLUasSDL1 → 10.0.0.3, fd00::1 +AMc61fS0kzlB5k8cReTbjBbK1FrWt12hvzPYwnMyF2vQ2 → 10.0.0.3, fd00::1 +AYxQdnOD7mLlbsG8l-tf03mVuBHrpq6re-XcrTv1A0n82 → 10.0.0.2 Constraints -constraint(A3L-mN23j3FJOvXZkXt45fsAMxeRa1RN4Tgd-m0_9Xp81 endorses any as **.test.mesh, 10.0.0.0/8, authority) +constraint(AF4BvPyD8vGkQ1ueSxfJ9ff_qTxJSr9-lJ9dCqGjth4M2 endorses any as **.test.mesh, 10.0.0.0/8, authority) Endorsements -Aj1Lizc7Xdv-DjFv6wlk9-ocRmzqejaHz-9H27ngqcXf1 endorses A1UqiwUlUpEHC0b_xtjSRHollrXSPmkXNasb40dX9xVi1 - as db.test.mesh, 10.0.0.2; - -Aj1Lizc7Xdv-DjFv6wlk9-ocRmzqejaHz-9H27ngqcXf1 endorses AoHQqnbGQu_8OBYXtpQQLvyrA3Mq7FDa-P5JRLUasSDL1 +AyRxMmLfKg1rSbhpX8BtfnF-CEwBwhEPe0cmT2Nb5rqg2 endorses AMc61fS0kzlB5k8cReTbjBbK1FrWt12hvzPYwnMyF2vQ2 as web.test.mesh, 10.0.0.3; + +AyRxMmLfKg1rSbhpX8BtfnF-CEwBwhEPe0cmT2Nb5rqg2 endorses AYxQdnOD7mLlbsG8l-tf03mVuBHrpq6re-XcrTv1A0n82 + as db.test.mesh, 10.0.0.2; "; assert_eq!(out, expected.strip_prefix('\n').assert()); @@ -849,16 +849,16 @@ Self: ┌─────────────────────┬──────────────┬────────────────┐ │ IMID │ Name │ IP │ ├─────────────────────┼──────────────┼────────────────┤ - │ Aj1Lizc7Xdv-DjFv... │ me.test.mesh │ 192.168.10.151 │ + │ AyRxMmLfKg1rSbhp... │ me.test.mesh │ 192.168.10.151 │ └─────────────────────┴──────────────┴────────────────┘ Others: ┌─────────────────────┬───────────────┬──────────┐ │ IMID │ Name │ IP │ ├─────────────────────┼───────────────┼──────────┤ - │ A1UqiwUlUpEHC0b_... │ db.test.mesh │ 10.0.0.2 │ + │ AMc61fS0kzlB5k8c... │ web.test.mesh │ 10.0.0.3 │ ├─────────────────────┼───────────────┼──────────┤ - │ AoHQqnbGQu_8OBYX... │ web.test.mesh │ 10.0.0.3 │ + │ AYxQdnOD7mLlbsG8... │ db.test.mesh │ 10.0.0.2 │ └─────────────────────┴───────────────┴──────────┘ "; diff --git a/src/verifier.rs b/src/verifier.rs index 9a1e83e..033d798 100644 --- a/src/verifier.rs +++ b/src/verifier.rs @@ -1,8 +1,6 @@ use crate::imid::Imid; use crate::trust_engine::TrustEngine; -use p256::ecdsa::signature::Verifier; -use p256::ecdsa::{Signature, VerifyingKey}; -use p256::PublicKey; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; use rustls::server::danger::{ClientCertVerified, ClientCertVerifier}; @@ -12,6 +10,7 @@ use rustls::{DigitallySignedStruct, Error, SignatureScheme}; use std::fmt; use std::fmt::{Debug, Formatter}; use std::sync::Arc; +use x509_cert::der::referenced::OwnedToRef; use x509_cert::der::{Decode, Encode}; use x509_cert::Certificate; @@ -111,23 +110,16 @@ impl IntermeshVerifier { cert: &Certificate, sig: Signature, bytes: &[u8], - ) -> Result { - let public_key_bytes = cert - .tbs_certificate - .subject_public_key_info - .subject_public_key - .as_bytes() - .ok_or_else(|| Error::InvalidCertificate(BadEncoding))?; + ) -> Result { + let public_key_info = cert.tbs_certificate.subject_public_key_info.owned_to_ref(); - let public_key = PublicKey::from_sec1_bytes(public_key_bytes) + let verifying_key = VerifyingKey::try_from(public_key_info) .map_err(|_| Error::InvalidCertificate(BadEncoding))?; - - let verifying_key = VerifyingKey::from(public_key); verifying_key .verify(bytes, &sig) .map_err(|_| Error::InvalidCertificate(BadSignature))?; - Ok(public_key) + Ok(verifying_key) } // Verify that the presented certificate is @@ -143,7 +135,7 @@ impl IntermeshVerifier { .to_der() .map_err(|_| Error::InvalidCertificate(BadEncoding))?; - let signature = Signature::from_der( + let signature = Signature::from_slice( parsed_cert .signature .as_bytes() @@ -182,7 +174,7 @@ impl IntermeshVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - if dss.scheme != SignatureScheme::ECDSA_NISTP256_SHA256 { + if dss.scheme != SignatureScheme::ED25519 { return Err(Error::PeerIncompatible( SignatureAlgorithmsExtensionRequired, )); @@ -191,7 +183,7 @@ impl IntermeshVerifier { let parsed_cert = Certificate::from_der(cert).map_err(|_| Error::InvalidCertificate(BadEncoding))?; - let signature = Signature::from_der(dss.signature()) + let signature = Signature::from_slice(dss.signature()) .map_err(|_| Error::InvalidCertificate(BadSignature))?; let _ = Self::verify_signature(&parsed_cert, signature, message)?; @@ -250,7 +242,7 @@ impl ServerCertVerifier for IntermeshVerifier { } fn supported_verify_schemes(&self) -> Vec { - vec![SignatureScheme::ECDSA_NISTP256_SHA256] + vec![SignatureScheme::ED25519] } } @@ -288,7 +280,7 @@ impl ClientCertVerifier for IntermeshVerifier { } fn supported_verify_schemes(&self) -> Vec { - vec![SignatureScheme::ECDSA_NISTP256_SHA256] + vec![SignatureScheme::ED25519] } fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {