signatures: Update pkcs8, and add ring backwards compatibility
This commit is contained in:
parent
906d576a27
commit
49663f4adf
@ -15,17 +15,19 @@ all-features = true
|
||||
|
||||
[features]
|
||||
compat = ["tracing"]
|
||||
ring-compat = ["subslice"]
|
||||
unstable-exhaustive-types = []
|
||||
unstable-msc2870 = []
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.13.0"
|
||||
ed25519-dalek = "1.0.1"
|
||||
pkcs8 = { version = "0.7.0", features = ["alloc"] }
|
||||
pkcs8 = { version = "0.9.0", features = ["alloc"] }
|
||||
# because dalek uses an older version of rand_core
|
||||
rand = { version = "0.7", features = ["getrandom"] }
|
||||
ruma-common = { version = "0.9.2", path = "../ruma-common" }
|
||||
serde_json = "1.0.60"
|
||||
sha2 = "0.9.5"
|
||||
subslice = { version = "0.2.3", optional = true }
|
||||
thiserror = "1.0.26"
|
||||
tracing = { version = "0.1.25", optional = true }
|
||||
|
@ -114,8 +114,8 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns
|
||||
///
|
||||
/// ```rust
|
||||
/// const PKCS8: &str = "\
|
||||
/// MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
|
||||
/// tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ\
|
||||
/// MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
|
||||
/// tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k=\
|
||||
/// ";
|
||||
///
|
||||
/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap();
|
||||
@ -428,8 +428,8 @@ pub fn reference_hash(
|
||||
/// # use ruma_signatures::{hash_and_sign_event, Ed25519KeyPair};
|
||||
/// #
|
||||
/// const PKCS8: &str = "\
|
||||
/// MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
|
||||
/// tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ\
|
||||
/// MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
|
||||
/// tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k=\
|
||||
/// ";
|
||||
///
|
||||
/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap();
|
||||
|
@ -6,14 +6,14 @@ use std::{
|
||||
};
|
||||
|
||||
use ed25519_dalek::{ExpandedSecretKey, PublicKey, SecretKey};
|
||||
use pkcs8::{
|
||||
der::{Decodable, Encodable},
|
||||
AlgorithmIdentifier, ObjectIdentifier, PrivateKeyInfo,
|
||||
};
|
||||
use pkcs8::{AlgorithmIdentifier, ObjectIdentifier, PrivateKeyInfo};
|
||||
use ruma_common::serde::Base64;
|
||||
|
||||
use crate::{signatures::Signature, Algorithm, Error, ParseError};
|
||||
|
||||
#[cfg(feature = "ring-compat")]
|
||||
pub mod compat;
|
||||
|
||||
/// A cryptographic key pair for digitally signing data.
|
||||
pub trait KeyPair: Sized {
|
||||
/// Signs a JSON object.
|
||||
@ -24,7 +24,7 @@ pub trait KeyPair: Sized {
|
||||
fn sign(&self, message: &[u8]) -> Signature;
|
||||
}
|
||||
|
||||
pub const ED25519_OID: ObjectIdentifier = ObjectIdentifier::new("1.3.101.112");
|
||||
pub const ED25519_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
|
||||
|
||||
/// An Ed25519 key pair.
|
||||
pub struct Ed25519KeyPair {
|
||||
@ -90,8 +90,31 @@ impl Ed25519KeyPair {
|
||||
/// generated from the private key. This is a fallback and extra validation against
|
||||
/// corruption or
|
||||
pub fn from_der(document: &[u8], version: String) -> Result<Self, Error> {
|
||||
let oak = PrivateKeyInfo::from_der(document).map_err(Error::DerParse)?;
|
||||
#[cfg(feature = "ring-compat")]
|
||||
use self::compat::CompatibleDocument;
|
||||
use pkcs8::der::Decode;
|
||||
|
||||
#[cfg(feature = "ring-compat")]
|
||||
let backing: Vec<u8>;
|
||||
let oak;
|
||||
|
||||
#[cfg(feature = "ring-compat")]
|
||||
{
|
||||
oak = match CompatibleDocument::from_bytes(document) {
|
||||
CompatibleDocument::WellFormed(bytes) => {
|
||||
PrivateKeyInfo::from_der(bytes).map_err(Error::DerParse)?
|
||||
}
|
||||
CompatibleDocument::CleanedFromRing(vec) => {
|
||||
backing = vec;
|
||||
|
||||
PrivateKeyInfo::from_der(&backing).map_err(Error::DerParse)?
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ring-compat"))]
|
||||
{
|
||||
oak = PrivateKeyInfo::from_der(document).map_err(Error::DerParse)?;
|
||||
}
|
||||
Self::from_pkcs8_oak(oak, version)
|
||||
}
|
||||
|
||||
@ -127,6 +150,8 @@ impl Ed25519KeyPair {
|
||||
///
|
||||
/// Returns an error if the generation failed.
|
||||
pub fn generate() -> Result<Vec<u8>, Error> {
|
||||
use pkcs8::der::Encode;
|
||||
|
||||
let secret = SecretKey::generate(&mut rand::rngs::OsRng);
|
||||
|
||||
let public = PublicKey::from(&secret);
|
||||
@ -139,7 +164,6 @@ impl Ed25519KeyPair {
|
||||
let pkinfo = PrivateKeyInfo {
|
||||
algorithm: AlgorithmIdentifier { oid: ED25519_OID, parameters: None },
|
||||
private_key: private.as_ref(),
|
||||
attributes: None,
|
||||
public_key: Some(public.as_bytes()),
|
||||
};
|
||||
|
||||
@ -191,19 +215,21 @@ pub type PublicKeySet = BTreeMap<String, Base64>;
|
||||
mod tests {
|
||||
use super::Ed25519KeyPair;
|
||||
|
||||
const RING_DOC: &[u8] = &[
|
||||
0x30, 0x53, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70, 0x04, 0x22, 0x04,
|
||||
0x20, 0x61, 0x9E, 0xD8, 0x25, 0xA6, 0x1D, 0x32, 0x29, 0xD7, 0xD8, 0x22, 0x03, 0xC6, 0x0E,
|
||||
0x37, 0x48, 0xE9, 0xC9, 0x11, 0x96, 0x3B, 0x03, 0x15, 0x94, 0x19, 0x3A, 0x86, 0xEC, 0xE6,
|
||||
0x2D, 0x73, 0xC0, 0xA1, 0x23, 0x03, 0x21, 0x00, 0x3D, 0xA6, 0xC8, 0xD1, 0x76, 0x2F, 0xD6,
|
||||
0x49, 0xB8, 0x4F, 0xF6, 0xC6, 0x1D, 0x04, 0xEA, 0x4A, 0x70, 0xA8, 0xC9, 0xF0, 0x8F, 0x96,
|
||||
0x7F, 0x6B, 0xD7, 0xDA, 0xE5, 0x2E, 0x88, 0x8D, 0xBA, 0x3E,
|
||||
const WELL_FORMED_DOC: &[u8] = &[
|
||||
0x30, 0x72, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70, 0x04, 0x22, 0x04,
|
||||
0x20, 0xD4, 0xEE, 0x72, 0xDB, 0xF9, 0x13, 0x58, 0x4A, 0xD5, 0xB6, 0xD8, 0xF1, 0xF7, 0x69,
|
||||
0xF8, 0xAD, 0x3A, 0xFE, 0x7C, 0x28, 0xCB, 0xF1, 0xD4, 0xFB, 0xE0, 0x97, 0xA8, 0x8F, 0x44,
|
||||
0x75, 0x58, 0x42, 0xA0, 0x1F, 0x30, 0x1D, 0x06, 0x0A, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D,
|
||||
0x01, 0x09, 0x09, 0x14, 0x31, 0x0F, 0x0C, 0x0D, 0x43, 0x75, 0x72, 0x64, 0x6C, 0x65, 0x20,
|
||||
0x43, 0x68, 0x61, 0x69, 0x72, 0x73, 0x81, 0x21, 0x00, 0x19, 0xBF, 0x44, 0x09, 0x69, 0x84,
|
||||
0xCD, 0xFE, 0x85, 0x41, 0xBA, 0xC1, 0x67, 0xDC, 0x3B, 0x96, 0xC8, 0x50, 0x86, 0xAA, 0x30,
|
||||
0xB6, 0xB6, 0xCB, 0x0C, 0x5C, 0x38, 0xAD, 0x70, 0x31, 0x66, 0xE1,
|
||||
];
|
||||
|
||||
const RING_PUBKEY: &[u8] = &[
|
||||
0x3D, 0xA6, 0xC8, 0xD1, 0x76, 0x2F, 0xD6, 0x49, 0xB8, 0x4F, 0xF6, 0xC6, 0x1D, 0x04, 0xEA,
|
||||
0x4A, 0x70, 0xA8, 0xC9, 0xF0, 0x8F, 0x96, 0x7F, 0x6B, 0xD7, 0xDA, 0xE5, 0x2E, 0x88, 0x8D,
|
||||
0xBA, 0x3E,
|
||||
const WELL_FORMED_PUBKEY: &[u8] = &[
|
||||
0x19, 0xBF, 0x44, 0x09, 0x69, 0x84, 0xCD, 0xFE, 0x85, 0x41, 0xBA, 0xC1, 0x67, 0xDC, 0x3B,
|
||||
0x96, 0xC8, 0x50, 0x86, 0xAA, 0x30, 0xB6, 0xB6, 0xCB, 0x0C, 0x5C, 0x38, 0xAD, 0x70, 0x31,
|
||||
0x66, 0xE1,
|
||||
];
|
||||
|
||||
#[test]
|
||||
@ -212,9 +238,37 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_key() {
|
||||
let keypair = Ed25519KeyPair::from_der(RING_DOC, "".to_owned()).unwrap();
|
||||
fn well_formed_key() {
|
||||
let keypair = Ed25519KeyPair::from_der(WELL_FORMED_DOC, "".to_owned()).unwrap();
|
||||
|
||||
assert_eq!(keypair.pubkey.as_bytes(), RING_PUBKEY);
|
||||
assert_eq!(keypair.pubkey.as_bytes(), WELL_FORMED_PUBKEY);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ring-compat")]
|
||||
mod ring_compat {
|
||||
use super::Ed25519KeyPair;
|
||||
|
||||
const RING_DOC: &[u8] = &[
|
||||
0x30, 0x53, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70, 0x04, 0x22,
|
||||
0x04, 0x20, 0x61, 0x9E, 0xD8, 0x25, 0xA6, 0x1D, 0x32, 0x29, 0xD7, 0xD8, 0x22, 0x03,
|
||||
0xC6, 0x0E, 0x37, 0x48, 0xE9, 0xC9, 0x11, 0x96, 0x3B, 0x03, 0x15, 0x94, 0x19, 0x3A,
|
||||
0x86, 0xEC, 0xE6, 0x2D, 0x73, 0xC0, 0xA1, 0x23, 0x03, 0x21, 0x00, 0x3D, 0xA6, 0xC8,
|
||||
0xD1, 0x76, 0x2F, 0xD6, 0x49, 0xB8, 0x4F, 0xF6, 0xC6, 0x1D, 0x04, 0xEA, 0x4A, 0x70,
|
||||
0xA8, 0xC9, 0xF0, 0x8F, 0x96, 0x7F, 0x6B, 0xD7, 0xDA, 0xE5, 0x2E, 0x88, 0x8D, 0xBA,
|
||||
0x3E,
|
||||
];
|
||||
|
||||
const RING_PUBKEY: &[u8] = &[
|
||||
0x3D, 0xA6, 0xC8, 0xD1, 0x76, 0x2F, 0xD6, 0x49, 0xB8, 0x4F, 0xF6, 0xC6, 0x1D, 0x04,
|
||||
0xEA, 0x4A, 0x70, 0xA8, 0xC9, 0xF0, 0x8F, 0x96, 0x7F, 0x6B, 0xD7, 0xDA, 0xE5, 0x2E,
|
||||
0x88, 0x8D, 0xBA, 0x3E,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn ring_key() {
|
||||
let keypair = Ed25519KeyPair::from_der(RING_DOC, "".to_owned()).unwrap();
|
||||
|
||||
assert_eq!(keypair.pubkey.as_bytes(), RING_PUBKEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
crates/ruma-signatures/src/keys/compat.rs
Normal file
61
crates/ruma-signatures/src/keys/compat.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use subslice::SubsliceExt as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CompatibleDocument<'a> {
|
||||
WellFormed(&'a [u8]),
|
||||
CleanedFromRing(Vec<u8>),
|
||||
}
|
||||
|
||||
impl<'a> CompatibleDocument<'a> {
|
||||
pub fn from_bytes(bytes: &'a [u8]) -> Self {
|
||||
if is_ring(bytes) {
|
||||
Self::CleanedFromRing(fix_ring_doc(bytes.to_vec()))
|
||||
} else {
|
||||
Self::WellFormed(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ring uses a very specific template to generate its documents,
|
||||
// and so this is essentially a sentinel value of that.
|
||||
//
|
||||
// It corresponds to CONTEXT-SPECIFIC[1](35) { BIT-STRING(32) {...} } in ASN.1
|
||||
//
|
||||
// A well-formed bit would look like just CONTEXT-SPECIFIC[1](32) { ... }
|
||||
//
|
||||
// Note: this is purely a sentinel value, don't take these bytes out of context
|
||||
// to detect or fiddle with the document.
|
||||
const RING_TEMPLATE_CONTEXT_SPECIFIC: &[u8] = &[0xA1, 0x23, 0x03, 0x21];
|
||||
|
||||
// A checked well-formed context-specific[1] prefix.
|
||||
const WELL_FORMED_CONTEXT_ONE_PREFIX: &[u8] = &[0x81, 0x21];
|
||||
|
||||
// If present, removes a malfunctioning pubkey suffix and adjusts the length at the start.
|
||||
fn fix_ring_doc(mut doc: Vec<u8>) -> Vec<u8> {
|
||||
assert!(!doc.is_empty());
|
||||
// Check if first tag is ASN.1 SEQUENCE
|
||||
assert_eq!(doc[0], 0x30);
|
||||
// Second byte asserts the length for the rest of the document
|
||||
assert_eq!(doc[1] as usize, doc.len() - 2);
|
||||
|
||||
let idx = doc
|
||||
.find(RING_TEMPLATE_CONTEXT_SPECIFIC)
|
||||
.expect("Expected to find ring template in doc, but found none.");
|
||||
|
||||
// Snip off the malformed bit.
|
||||
let suffix = doc.split_off(idx);
|
||||
|
||||
// Feed back an actual well-formed prefix.
|
||||
doc.extend(WELL_FORMED_CONTEXT_ONE_PREFIX);
|
||||
|
||||
// Then give it the actual public key.
|
||||
doc.extend(&suffix[4..]);
|
||||
|
||||
doc[1] = doc.len() as u8 - 2;
|
||||
|
||||
doc
|
||||
}
|
||||
|
||||
fn is_ring(bytes: &[u8]) -> bool {
|
||||
bytes.find(RING_TEMPLATE_CONTEXT_SPECIFIC).is_some()
|
||||
}
|
@ -105,7 +105,7 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use base64::{decode_config, STANDARD_NO_PAD};
|
||||
use pkcs8::{der::Decodable, PrivateKeyInfo};
|
||||
use pkcs8::{der::Decode, PrivateKeyInfo};
|
||||
use ruma_common::{serde::Base64, RoomVersionId};
|
||||
use serde_json::{from_str as from_json_str, to_string as to_json_string};
|
||||
|
||||
@ -114,8 +114,8 @@ mod tests {
|
||||
};
|
||||
|
||||
const PKCS8: &str = "\
|
||||
MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
|
||||
tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ\
|
||||
MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
|
||||
tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k=\
|
||||
";
|
||||
|
||||
/// Convenience method for getting the public key as a string
|
||||
|
@ -91,6 +91,11 @@ compat = [
|
||||
"ruma-state-res/compat",
|
||||
]
|
||||
|
||||
# Specific compatibility for past ring public/private key documents.
|
||||
ring-compat = [
|
||||
"ruma-signatures/ring-compat"
|
||||
]
|
||||
|
||||
# Helper features that aren't exactly part of the spec but could be helpful
|
||||
# for crate consumers
|
||||
appservice-api-helper = ["ruma-appservice-api/helper"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user