diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index bbabf092e0..9bcf2c5498 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -169,6 +169,7 @@ nextest nolfs notadb nsec +nsecs OCSP Oleksandr oneshot @@ -239,8 +240,8 @@ slotno smac stevenj stringzilla -subsec subnetwork +subsec symlinkat syscall tacho diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/.pages b/docs/src/architecture/08_concepts/rbac_id_uri/.pages similarity index 66% rename from docs/src/architecture/08_concepts/rbac_kid_uri/.pages rename to docs/src/architecture/08_concepts/rbac_id_uri/.pages index 0bcc138749..35d55f1a31 100644 --- a/docs/src/architecture/08_concepts/rbac_kid_uri/.pages +++ b/docs/src/architecture/08_concepts/rbac_id_uri/.pages @@ -1,3 +1,3 @@ title: RBAC KID (Key Identifier) URI arrange: - - kiduri.md + - catalyst-id-uri.md diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md similarity index 50% rename from docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md rename to docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index fd09c68470..1c0bcc21a6 100644 --- a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -1,5 +1,5 @@ --- -Title: RBAC Key Identifier URI Specification +Title: RBAC Catalyst Identifier URI Specification Category: Catalyst Status: Proposed Authors: @@ -11,6 +11,8 @@ Created: 2025-01-05 License: CC-BY-4.0 --- + + * [Abstract](#abstract) * [Motivation: why is this CIP necessary?](#motivation-why-is-this-cip-necessary) * [Specification](#specification) @@ -20,8 +22,7 @@ License: CC-BY-4.0 * [`authority` - `host`](#authority---host) * [List of defined hosts](#list-of-defined-hosts) * [`authority` - `userinfo`](#authority---userinfo) - * [Lists of defined subnetwork `userinfo` values](#lists-of-defined-subnetwork-userinfo-values) - * [Cardano](#cardano) + * [Example `userinfo` with a `hostname`](#example-userinfo-with-a-hostname) * [`path`](#path) * [Reference Implementation](#reference-implementation) * [Test Vectors](#test-vectors) @@ -33,39 +34,44 @@ License: CC-BY-4.0 ## Abstract -Definition of a [URI] which allows for RBAC keys used for different purposes to be easily and +Definition of a [URI], which allows for RBAC keys used for different purposes to be easily and unambiguously identified. ## Motivation: why is this CIP necessary? -There is a need to identify which Key from a RBAC registration was used to sign data. +There is a need to identify which RBAC Registration is referenced, +or which Key from a RBAC registration was used to sign data. RBAC defines a universal keychain of different keys that can be used for different purposes. They can be used not only for Signatures, but also Encryption. -Therefore, there needs to be an unambiguous and easy to lookup identifier to signify which key was -used for a particular purpose. +Sometimes all that is required is to identify the individual keychain. +Other times a specific key on the chain needs to be referenced. + +Therefore, there needs to be an unambiguous and easy to lookup identifier to signify which keychain, +or key in a particular chain was used for a particular purpose. -This document defines a [URI] scheme to unambiguously define a particular key with reference to a -particular RBAC keychain. +This document defines a [URI] scheme to unambiguously define a keychain or a specific key within the keychain. ## Specification ### URI -The RBAC Kid is formatted using a [Universal Resource Identifier]. +The Catalyst RBAC ID is formatted using a [Universal Resource Identifier]. Refer to [RFC3986] for the specification of the URI format. ### `scheme` -The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `kid.catalyst-rbac`; +The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `id.catalyst`. + +When used as a Catalyst ID, where only catalyst IDs would be used, the scheme can be omitted. ### `authority` The [authority](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2) references the blockchain or network the key was registered within. -It is perfectly valid for a Kid to reference a different network than the place where the Key is used. -For example, a `cardano` KID can be used to post documents to `IPFS`. +It is perfectly valid for an ID Uri to reference a different network than the place where the ID or Key is used. +For example, a `cardano` ID can be used to post documents to `IPFS`. Its purpose is to define WHERE the key was registered, and nothing more. The Authority will consist of a `host` and optional `userinfo`. @@ -76,32 +82,45 @@ The [host](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2) refers to the network type where the RBAC registration was made. It **IS NOT** resolvable with **DNS**, and **IS NOT** a public host name. It is used as a decentralized network identifier. -The consumer of the `KID` must be able to resolve these host names. +The consumer of the `ID` must be able to resolve these host names to known and supported blockchain networks. + +The hostname may have one or more subdomains which could specify side-chains of a particular network, +or test networks. ##### List of defined hosts | `host` | Description | | --- | --- | | `cardano` | Cardano Blockchain | +| `preprod.cardano` | Preprod Cardano Blockchain test network | +| `preview.cardano` | Preview Cardano Blockchain test network | | `midnight` | Midnight Blockchain | | `ethereum` | Ethereum Blockchain | | `cosmos` | Cosmos Blockchain | +This list is indicative of the host names that can be used, any hostname is valid provided it is +capable of storing catalyst RBAC registration keychains. + #### `authority` - `userinfo` -The [userinfo](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1) -is used to distinguish a subnetwork from the primary main network. -The absence of `userinfo` is used to indicate the primary main network. +The [userinfo] is used to hold a user defined readable name that can be attached to the keychain. +It may contain an optional `nonce` which is separated from the user's name by a `:` and replaces a +traditional password used for HTTP basic authentication. -##### Lists of defined subnetwork `userinfo` values +Because the name is not unique, and is provided by the user, it is informational only. +A URI is identical, provided the hostname and path are the same, the [userinfo] does not play +a part in validating or finding the catalyst keychain being referenced. -###### Cardano +The `nonce` part contained in the `password` component of the username *MUST* be an integer, +and it is the number of seconds since 1970 UTC, when the Catalyst ID URI was generated. -| `userinfo` | Description | -| --- | --- | -| `preprod` | Cardano Pre-Production Network | -| `preview` | Cardano Preview Network | -| 0x | Cardano network identified by this magic number in hex | +Applications which use the `nonce` will define its use, anything that does not use the `nonce` will ignore it. + +##### Example `userinfo` with a `hostname` + +* `anne@cardano` - username `anne` no nonce. +* `blake:1737101079@midnight` - username `blake` with nonce 1737101079. +* `:173710179@ethereum` - no username with nonce 173710179. ### `path` @@ -115,13 +134,17 @@ The overall `path` specification is: `//#encr * This does not change, even if the Initial Role 0 key is revoked. * This allows for an unambiguous identifier for the RBAC keychain. * It is not necessarily the key being identified. -* `` - This is the Role number being used. + * An example Role 0 Key is `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` +* `` - *Optional* This is the Role number being used. * It is a positive number, starting at 0, and no greater than 65535. -* `` - This is the rotation of the defined role key being identified. + * If it is not defined, then its default value is 0. + * If it is not defined, there can be no `` part of the path following. +* `` - *Optional* This is the rotation of the defined role key being identified. * It starts at 0 for the first published key for the role, and increments by one for each subsequent published rotation. * This number refers to the published sequence of keys for the role in the RBAC registration keychain, not the index used in the key derivation. * It is positive and no greater than 65535. + * If not present, it defaults to 0. * `#encrypt` - [Fragment](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5) disambiguates Encryption Public Keys from signing public keys. * Roles can have 1 active public signing key, and 1 active public encryption key. @@ -134,26 +157,52 @@ The first implementation will be Catalyst Voices. ## Test Vectors -* `kid.catalyst-rbac://cardano//0/0` +* `id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` + * A Signing key registered on the Cardano Main network. + * Role 0 - Rotation 0. + * `username` - undefined. + * `nonce` - undefined. + * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0` or + `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0`. +* `id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0` + * A Signing key registered on the Cardano Main network. + * Role 0 - Rotation 0. + * `username` - undefined. + * `nonce` - undefined. + * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0` or + `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`. +* `id.catalyst://gary@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. - In this example, it is exactly the same as the ``. -* `kid.catalyst-rbac://preprod@cardano//7/3` + * `username` - `gary`. + * `nonce` - undefined. + * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` or + `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0`. +* `id.catalyst://faith@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3` * A Signing key registered on the Cardano pre-production network. * Role 7 - Rotation 3. - The Key for Role 7, and its third published rotation + * `username` - `faith` + * `nonce` - undefined. + * The Key for Role 7, and its third published rotation (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). -* `kid.catalyst-rbac://preprod@cardano//2/0#encrypt` +* `id.catalyst://faith:173710179@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt` * A Public Encryption key registered on the Cardano pre-production network. * Role 2 - Rotation 0. - The initially published Public Encryption Key for Role 2. -* `kid.catalyst-rbac://midnight//0/1` + * `username` - `faith` + * `nonce` - 173710179. + * The initially published Public Encryption Key for Role 2. +* `id.catalyst://:173710179@midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1` * A Signing key registered on the Midnight Blockchain Main network * Role 0 - Rotation 1. - In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. -* `kid.catalyst-rbac://midnight//2/1#encrypt` - * A public encryption key registered on the Midnight Blockchain Main network. + * `username` - undefined. + * `nonce` - 173710179. + * In this example, it is NOT the same as the `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`, + as it identifies the first rotation after `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`. +* `id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt` + * A Public Encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. + * `username` - undefined. + * `nonce` - undefined. ## Rationale: how does this CIP achieve its goals? @@ -178,3 +227,4 @@ This document is licensed under [CC-BY-4.0](https://creativecommons.org/licenses [Universal Resource Identifier]: https://datatracker.ietf.org/doc/html/rfc3986 [RFC3986]: https://datatracker.ietf.org/doc/html/rfc3986 [Base64 URL]: https://datatracker.ietf.org/doc/html/rfc4648#section-5 +[userinfo]: (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1) diff --git a/rust/cardano-blockchain-types/deps.tmp b/rust/cardano-blockchain-types/deps.tmp deleted file mode 100644 index f65f5bfb55..0000000000 --- a/rust/cardano-blockchain-types/deps.tmp +++ /dev/null @@ -1,61 +0,0 @@ -rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.8" } - -thiserror = "1.0.64" -tokio = { version = "1.40.0", features = [ - "macros", - "rt", - "net", - "rt-multi-thread", -] } -tracing = "0.1.40" -tracing-log = "0.2.0" -dashmap = "6.1.0" -url = "2.5.2" -anyhow = "1.0.89" -chrono = "0.4.38" -async-trait = "0.1.83" -dirs = "5.0.1" -futures = "0.3.31" -humantime = "2.1.0" -crossbeam-skiplist = "0.1.3" -crossbeam-channel = "0.5.13" -crossbeam-epoch = "0.9.18" -strum = "0.26.3" -ouroboros = "0.18.4" -hex = "0.4.3" -rayon = "1.10.0" -serde = "1.0.210" -serde_json = "1.0.128" -mimalloc = { version = "0.1.43", optional = true } -memx = "0.1.32" -fmmap = { version = "0.3.3", features = ["sync", "tokio-async"] } -minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } -zstd = "0.13.2" -ed25519-dalek = "2.1.1" -blake2b_simd = "1.0.2" -num-traits = "0.2.19" -logcall = "0.1.9" -tar = "0.4.42" -ureq = { version = "2.10.1", features = ["native-certs"] } -http = "1.1.0" -hickory-resolver = { version = "0.24.1", features = ["dns-over-rustls"] } -moka = { version = "0.12.8", features = ["sync"] } - -hex = "0.4.3" -anyhow = "1.0.89" -strum_macros = "0.26.4" -regex = "1.11.0" -minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } -brotli = "7.0.0" -zstd = "0.13.2" -x509-cert = "0.2.5" -der-parser = "9.0.0" -bech32 = "0.11.0" -dashmap = "6.1.0" -blake2b_simd = "1.0.2" -tracing = "0.1.40" -ed25519-dalek = "2.1.1" -uuid = "1.11.0" - -c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git" , tag = "v0.0.3" } -pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } diff --git a/rust/catalyst-types/Cargo.toml b/rust/catalyst-types/Cargo.toml index 7f1d06f936..9fccfc70c9 100644 --- a/rust/catalyst-types/Cargo.toml +++ b/rust/catalyst-types/Cargo.toml @@ -26,9 +26,10 @@ num-traits = "0.2.19" orx-concurrent-vec = { version = "3.2.0", features = ["serde"] } pallas-crypto = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } serde = { version = "1.0.217", features = ["derive", "rc"] } -thiserror = "2.0.9" +thiserror = "2.0.11" base64-url = "3.0.0" -uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } +uuid = { version = "1.12.0", features = ["v4", "v7", "serde"] } +chrono = "0.4.39" fmmap = { version = "0.3.3", features = ["sync", "tokio-async"] } once_cell = "1.20.2" tracing = "0.1.41" diff --git a/rust/catalyst-types/src/kid_uri/errors.rs b/rust/catalyst-types/src/id_uri/errors.rs similarity index 79% rename from rust/catalyst-types/src/kid_uri/errors.rs rename to rust/catalyst-types/src/id_uri/errors.rs index 7730c6a1a4..80d8a00338 100644 --- a/rust/catalyst-types/src/kid_uri/errors.rs +++ b/rust/catalyst-types/src/id_uri/errors.rs @@ -7,13 +7,15 @@ use super::{key_rotation::KeyRotationError, role_index::RoleIndexError}; /// Errors that can occur when parsing a `KidUri` #[derive(Display, Error, Debug)] -pub enum KidUriError { +pub enum IdUriError { /// Invalid KID URI - InvalidURI(#[from] fluent_uri::error::ParseError), - /// Invalid Scheme, not a KID URI + InvalidURI(#[from] fluent_uri::error::ParseError), + /// Invalid Scheme, not a ID URI InvalidScheme, /// Network not defined in URI NoDefinedNetwork, + /// Invalid Nonce + InvalidNonce, /// Path of URI is invalid InvalidPath, /// Role 0 Key in path is invalid @@ -30,4 +32,6 @@ pub enum KidUriError { InvalidRotationValue(#[from] KeyRotationError), /// Encryption key Identifier Fragment is not valid InvalidEncryptionKeyFragment, + /// Invalid Text encoding + InvalidTextEncoding(#[from] std::string::FromUtf8Error), } diff --git a/rust/catalyst-types/src/kid_uri/key_rotation.rs b/rust/catalyst-types/src/id_uri/key_rotation.rs similarity index 74% rename from rust/catalyst-types/src/kid_uri/key_rotation.rs rename to rust/catalyst-types/src/id_uri/key_rotation.rs index 48b262f2bd..44887242dc 100644 --- a/rust/catalyst-types/src/kid_uri/key_rotation.rs +++ b/rust/catalyst-types/src/id_uri/key_rotation.rs @@ -21,6 +21,23 @@ pub enum KeyRotationError { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct KeyRotation(u16); +impl KeyRotation { + /// Default Key Rotation + pub const DEFAULT: KeyRotation = KeyRotation(0); + + /// Is the `KeyRotation` the default value + #[must_use] + pub fn is_default(self) -> bool { + self == Self::DEFAULT + } +} + +impl Default for KeyRotation { + fn default() -> Self { + Self::DEFAULT + } +} + impl From for KeyRotation { fn from(value: u16) -> Self { Self(value) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs new file mode 100644 index 0000000000..af664f80b5 --- /dev/null +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -0,0 +1,713 @@ +//! Catalyst ID URI. + +// cspell: words userinfo rngs Fftx csprng + +pub mod errors; +pub mod key_rotation; +pub mod role_index; + +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use chrono::{DateTime, Duration, Utc}; +use ed25519_dalek::VerifyingKey; +use fluent_uri::{ + component::Scheme, + encoding::{ + encoder::{Fragment, Path}, + EStr, + }, + Uri, +}; +use key_rotation::KeyRotation; +use role_index::RoleIndex; + +/// Catalyst ID +/// +/// Identity of Catalyst Registration. +/// Optionally also identifies a specific Signed Document Key +#[derive(Debug, Clone, PartialEq, Hash)] +#[allow(clippy::module_name_repetitions)] +pub struct IdUri { + /// Username + username: Option, + /// Nonce (like the password in http basic auth, but NOT a password, just a nonce) + nonce: Option>, + /// Network + network: String, + /// Sub Network + subnet: Option, + /// Role0 Public Key. + role0_pk: VerifyingKey, + /// User Role specified for the current document. + role: RoleIndex, + /// Role Key Rotation count + rotation: KeyRotation, + /// Indicates whether this key is an encryption key. + /// - `true`: The key is used for encryption. + /// - `false`: The key is used for signing (signature key). + encryption: bool, + /// Indicates if this is an `id` type, or a `uri` type. + /// Used by the serialization functions. + /// `true` = format as an `Id` + /// `false` = format as a `Uri` + id: bool, +} + +impl IdUri { + /// Encryption Key Identifier Fragment + const ENCRYPTION_FRAGMENT: &EStr = EStr::new_or_panic("encrypt"); + /// Maximum allowable Nonce Value + /// * Monday, January 1, 2125 12:00:00 AM + const MAX_NONCE: i64 = 4_891_363_200; + /// Minimum allowable Nonce Value + /// * Wednesday, January 1, 2025 12:00:00 AM + const MIN_NONCE: i64 = 1_735_689_600; + /// URI scheme for Catalyst + const SCHEME: &Scheme = Scheme::new_or_panic("id.catalyst"); + + /// Get the cosmetic username from the URI. + #[must_use] + pub fn username(&self) -> Option { + self.username.clone() + } + + /// Get the nonce from the URI. + #[must_use] + pub fn nonce(&self) -> Option> { + self.nonce + } + + /// Get the network the `IdUri` is referencing the registration to. + #[must_use] + pub fn network(&self) -> (String, Option) { + (self.network.clone(), self.subnet.clone()) + } + + /// Is the key a signature type key. + #[must_use] + pub fn is_signature_key(&self) -> bool { + !self.encryption + } + + /// Is the key an encryption type key. + #[must_use] + pub fn is_encryption_key(&self) -> bool { + self.encryption + } + + /// Get the Initial Role 0 Key of the registration + #[must_use] + pub fn role0_pk(&self) -> VerifyingKey { + self.role0_pk + } + + /// Get the role index and its rotation count + #[must_use] + pub fn role_and_rotation(&self) -> (RoleIndex, KeyRotation) { + (self.role, self.rotation) + } + + /// Create a new `IdUri` for a Signing Key + #[must_use] + pub fn new(network: &str, subnet: Option<&str>, role0_pk: VerifyingKey) -> Self { + Self { + username: None, // Default to Not set, use `with_username` if required. + nonce: None, // Default to Not set, use `with_nonce` if required. + network: network.to_string(), + subnet: subnet.map(str::to_string), + role0_pk, + role: RoleIndex::default(), // Defaulted, use `with_role()` to change it. + rotation: KeyRotation::default(), // Defaulted, use `with_rotation()` to change it. + encryption: false, // Defaulted, use `with_encryption()` to change it. + id: false, // Default to `URI` formatted. + } + } + + /// The `IdUri` is formatted as a URI. + #[must_use] + pub fn as_uri(self) -> Self { + Self { id: false, ..self } + } + + /// The `IdUri` is formatted as a id. + #[must_use] + pub fn as_id(self) -> Self { + Self { id: true, ..self } + } + + /// Was `IdUri` formatted as a id when it was parsed. + #[must_use] + pub fn is_id(self) -> bool { + self.id + } + + /// Add or change the username in a Catalyst ID URI. + #[must_use] + pub fn with_username(self, name: &str) -> Self { + Self { + username: Some(name.to_string()), + ..self + } + } + + /// Add or change the username in a Catalyst ID URI. + #[must_use] + pub fn without_username(self) -> Self { + Self { + username: None, + ..self + } + } + + /// Add or change the nonce (a unique identifier for a data update) to a specific + /// value in a Catalyst `IdUri`. + /// + /// This method is intended for use with trusted data where the nonce is known and + /// verified beforehand. It ensures that the provided nonce is within the valid + /// range, clamping it if necessary between `MIN_NONCE` and `MAX_NONCE`. + /// Properly generated or trusted nonces will not be altered by this function. + /// + /// # Parameters + /// - `nonce`: A `DateTime` representing the specific nonce value to set in the + /// Catalyst `IdUri`. This should be a valid UTC datetime. + /// + /// # Returns + /// The updated Catalyst `IdUri` with the specified nonce, if it was within the + /// allowed range; otherwise, it will be updated with a clamped value of the + /// nonce. + /// + /// # Safety + /// - **Pre-validation of the nonce is required**: If you are working with untrusted + /// data, ensure that the nonce has been pre-validated and take appropriate action + /// before calling this function. + #[must_use] + pub fn with_specific_nonce(self, nonce: DateTime) -> Self { + let secs = nonce.timestamp(); + let clamped_secs = secs.clamp(Self::MIN_NONCE, Self::MAX_NONCE); + + let nonce = { + if clamped_secs == secs { + Some(nonce) + } else { + DateTime::::from_timestamp(clamped_secs, 0) + } + }; + + Self { nonce, ..self } + } + + /// Add or change the nonce in a Catalyst ID URI. The nonce will be set to the current + /// UTC time when this method is called. + /// + /// This function returns a new instance of the type with the nonce field updated to + /// the current UTC time. + /// + /// # Examples + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// use chrono::Utc; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// assert!(id_uri.nonce().is_none()); + /// let id_uri_with_nonce = id_uri.with_nonce(); + /// assert!(id_uri_with_nonce.nonce().is_some()); + /// ``` + #[must_use] + pub fn with_nonce(self) -> Self { + self.with_specific_nonce(Utc::now()) + } + + /// Set that there is no Nonce in the ID or URI + /// Represents an ID or URI without a Nonce. + /// + /// This method creates a new instance of the type, but sets the nonce field to + /// `None`. The rest of the fields are inherited from the original instance. + /// + /// # Examples + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// use chrono::{DateTime, Duration, Utc}; + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap() + /// .with_nonce(); + /// + /// let id_uri_without_nonce = id_uri.without_nonce(); + /// assert_eq!(id_uri_without_nonce.nonce(), None); + /// ``` + #[must_use] + pub fn without_nonce(self) -> Self { + Self { + nonce: None, + ..self + } + } + + /// Set that the `IdUri` is used to identify an encryption key. + /// + /// This method sets `IdUri` is identifying an encryption key. + /// + /// # Returns + /// + /// A new instance of the type with the updated encryption flag. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// assert_eq!(id_uri.is_encryption_key(), false); + /// + /// let id_uri = id_uri.with_encryption(); + /// assert_eq!(id_uri.is_encryption_key(), true); + /// ``` + #[must_use] + pub fn with_encryption(self) -> Self { + Self { + encryption: true, + ..self + } + } + + /// Set that the `IdUri` is not for encryption + /// + /// This method sets `IdUri` is not identifying an encryption key. + /// + /// # Returns + /// + /// A new instance of the type with the updated encryption flag. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE#encrypt" + /// .parse::() + /// .unwrap(); + /// assert_eq!(id_uri.is_encryption_key(), true); + /// + /// let id_uri = id_uri.without_encryption(); + /// assert_eq!(id_uri.is_encryption_key(), false); + /// ``` + #[must_use] + pub fn without_encryption(self) -> Self { + Self { + encryption: false, + ..self + } + } + + /// Set the role explicitly. + /// + /// This method sets the role field to the specified value while leaving other + /// fields unchanged. + /// + /// # Parameters + /// - `role`: The new value for the role field. + /// + /// # Returns + /// + /// A new instance of the type with the updated role field. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::{role_index::RoleIndex, IdUri}; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// let new_role: RoleIndex = 5.into(); + /// let id_uri_with_role = id_uri.with_role(new_role); + /// let (role, _) = id_uri_with_role.role_and_rotation(); + /// assert_eq!(role, new_role); + /// ``` + #[must_use] + pub fn with_role(self, role: RoleIndex) -> Self { + Self { role, ..self } + } + + /// Set the rotation explicitly. + /// + /// This method sets the rotation field to the specified value while leaving other + /// fields unchanged. + /// + /// # Parameters + /// - `rotation`: The new value for the rotation field. 0 = First Key, 1+ is each + /// subsequent rotation. + /// + /// # Returns + /// A new instance of the type with the updated rotation field. + /// + /// # Examples + /// ```rust + /// use catalyst_types::id_uri::{key_rotation::KeyRotation, IdUri}; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// let new_rotation: KeyRotation = 4.into(); + /// let id_uri_with_rotation = id_uri.with_rotation(new_rotation); + /// let (_, rotation) = id_uri_with_rotation.role_and_rotation(); + /// assert_eq!(rotation, new_rotation); + /// ``` + #[must_use] + pub fn with_rotation(self, rotation: KeyRotation) -> Self { + Self { rotation, ..self } + } + + /// Check if the URI has a nonce that falls within the defined boundary around `now()` + /// + /// This function checks whether the nonce (if present) is within the specified time + /// range relative to the current system time (`now`). The range is determined by + /// adding and subtracting the given durations (`past` and `future`) from the current + /// time. If a URI does not have a defined nonce, this function will always return + /// `false`. + /// + /// # Arguments + /// + /// * `self`: A reference to the URI object that contains the potential nonce. + /// * `past`: The duration by which we look back in time from the current moment + /// (`now`). (Positive Duration) + /// * `future`: The duration by which we look forward in time from the current moment + /// (`now`). (Positive Duration) + /// + /// # Returns + /// + /// A boolean value: + /// - `true` if the nonce is within the specified range relative to `now()`. + /// - `false` if there is no nonce defined or if the nonce falls outside the specified + /// range. + /// + /// If the URI does not have a nonce defined, this function returns `false` + /// immediately because it cannot perform the check without a valid nonce present. + /// This behavior ensures that the absence of a nonce will fail any required range + /// checks when such checks are expected according to the function's contract. + /// + /// # Examples + /// + /// ``` + /// use catalyst_types::id_uri::IdUri; + /// use chrono::{DateTime, Duration, Utc}; + /// let uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap() + /// .with_nonce(); + /// // true, within range + /// assert!(uri.is_nonce_in_range(chrono::Duration::hours(1), chrono::Duration::minutes(5))); + /// + /// // Change the nonce to be 1970/1/1 00:00:00 + /// let uri = uri.with_specific_nonce(DateTime::::MIN_UTC); + /// // false, outside range + /// assert!(!uri.is_nonce_in_range(chrono::Duration::hours(1), chrono::Duration::minutes(5))); + /// ``` + #[must_use] + pub fn is_nonce_in_range(&self, past: Duration, future: Duration) -> bool { + if let Some(nonce) = self.nonce { + let now = Utc::now(); + let start_time = now - past; + let end_time = now + future; + (start_time..=end_time).contains(&nonce) + } else { + // No nonce defined, so we say that this fails. + // Prevents an absent Nonce from passing range checks when its required. + false + } + } + + /// Converts the `IdUri` to its shortest form. + /// This method returns a new instance of the type with no role information, no + /// scheme, no username, no nonce, and no encryption settings. It effectively + /// strips away all additional metadata to provide a most generalized form of the + /// Catalyst ID. + /// + /// # Returns + /// + /// A new `IdUri` instance representing the shortest form of the current `IdUri`. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::{key_rotation::KeyRotation, role_index::RoleIndex, IdUri}; + /// + /// let id_uri = + /// "id.catalyst://user:1735689600@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/5" + /// .parse::() + /// .unwrap(); + /// + /// let short_id = id_uri.as_short_id(); + /// assert_eq!( + /// short_id.role_and_rotation(), + /// (RoleIndex::default(), KeyRotation::default()) + /// ); + /// assert_eq!(short_id.username(), None); + /// assert_eq!(short_id.nonce(), None); + /// assert_eq!(short_id.is_encryption_key(), false); + /// + /// let short_id_str = format!("{short_id}"); + /// assert_eq!( + /// short_id_str, + /// "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// ) + /// ``` + #[must_use] + pub fn as_short_id(&self) -> Self { + self.clone() + .with_role(RoleIndex::default()) + .with_rotation(KeyRotation::default()) + .without_username() + .without_nonce() + .without_encryption() + .as_id() + } +} + +impl FromStr for IdUri { + type Err = errors::IdUriError; + + /// This will parse a URI or a RAW ID. + /// The only difference between them is a URI has the scheme, a raw ID does not. + fn from_str(s: &str) -> Result { + // Did we serialize an ID? + let mut id = false; + + // Check if we have a scheme, and if not default it to the catalyst ID scheme. + let raw_uri = { + if s.contains("://") { + s.to_owned() + } else { + id = true; + // It might be a RAW ID, so try and parse with the correct scheme. + format!("{}://{}", IdUri::SCHEME, s) + } + }; + + let uri = Uri::parse(raw_uri)?; + + // Check if its the correct scheme. + if uri.scheme() != IdUri::SCHEME { + return Err(errors::IdUriError::InvalidScheme); + } + + // Decode the network and subnet + let auth = uri + .authority() + .ok_or(errors::IdUriError::NoDefinedNetwork)?; + let (subnet, network) = { + let host = auth.host(); + if let Some((subnet, host)) = host.rsplit_once('.') { + (Some(subnet), host) + } else { + (None, host) + } + }; + + let (username, nonce) = { + if let Some(userinfo) = auth.userinfo() { + if let Some((username, nonce)) = userinfo.split_once(':') { + let username = username.decode().into_string_lossy().to_string(); + let nonce_str = nonce.decode().into_string_lossy().to_string(); + + let nonce_val: i64 = nonce_str + .parse() + .map_err(|_| errors::IdUriError::InvalidNonce)?; + if !(IdUri::MIN_NONCE..=IdUri::MAX_NONCE).contains(&nonce_val) { + return Err(errors::IdUriError::InvalidNonce); + } + + let nonce = DateTime::::from_timestamp(nonce_val, 0); + + (Some(username), nonce) + } else { + let username = userinfo.decode().into_string_lossy().to_string(); + (Some(username), None) + } + } else { + (None, None) + } + }; + + let path: Vec<&EStr> = uri.path().split('/').collect(); + + // Can ONLY have 3 path components, no more and no less + // Less than 3 handled by errors below (4 because of leading `/` in path). + if path.len() > 4 { + return Err(errors::IdUriError::InvalidPath); + }; + + // Decode and validate the Role0 Public key from the path + let encoded_role0_key = path.get(1).ok_or(errors::IdUriError::InvalidRole0Key)?; + let decoded_role0_key = + base64_url::decode(encoded_role0_key.decode().into_string_lossy().as_ref())?; + let role0_pk = crate::conversion::vkey_from_bytes(&decoded_role0_key) + .or(Err(errors::IdUriError::InvalidRole0Key))?; + + // Decode and validate the Role Index from the path. + let role_index: RoleIndex = { + if let Some(encoded_role_index) = path.get(2) { + let decoded_role_index = encoded_role_index.decode().into_string_lossy(); + decoded_role_index.parse::()? + } else { + RoleIndex::default() + } + }; + + // Decode and validate the Rotation Value from the path. + let rotation: KeyRotation = { + if let Some(encoded_rotation) = path.get(3) { + let decoded_rotation = encoded_rotation.decode().into_string_lossy(); + decoded_rotation.parse::()? + } else { + KeyRotation::default() + } + }; + + let cat_id = { + let mut cat_id = Self::new(network, subnet, role0_pk) + .with_role(role_index) + .with_rotation(rotation); + + if uri.has_fragment() { + if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { + cat_id = cat_id.with_encryption(); + } else { + return Err(errors::IdUriError::InvalidEncryptionKeyFragment); + } + } + + if let Some(username) = username { + cat_id = cat_id.with_username(&username); + } + + if let Some(nonce) = nonce { + cat_id = cat_id.with_specific_nonce(nonce); + } + + // Default to URI, so only set it as an ID if its not a URI. + if id { + cat_id = cat_id.as_id(); + } + + cat_id + }; + + Ok(cat_id) + } +} + +impl Display for IdUri { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + if !self.id { + write!(f, "{}://", Self::SCHEME.as_str())?; + } + + let mut needs_at = false; + if let Some(username) = &self.username { + write!(f, "{username}")?; + needs_at = true; + } + + if let Some(nonce) = self.nonce { + let timestamp = nonce.timestamp(); + write!(f, ":{timestamp}")?; + needs_at = true; + } + + // If we had a username OR a nonce, then we need an `@` to separate from the hostname. + if needs_at { + write!(f, "@")?; + } + + if let Some(subnet) = &self.subnet { + write!(f, "{subnet}.")?; + } + write!( + f, + "{}/{}", + self.network, + base64_url::encode(self.role0_pk.as_bytes()), + )?; + + // Role and Rotation are only serialized if its NOT and ID or they are not the defaults. + if !self.role.is_default() || !self.rotation.is_default() || !self.id { + write!(f, "/{}", self.role)?; + if !self.rotation.is_default() || !self.id { + write!(f, "/{}", self.rotation)?; + } + } + + if self.encryption { + write!(f, "#{}", Self::ENCRYPTION_FRAGMENT)?; + } + Ok(()) + } +} + +impl TryFrom<&[u8]> for IdUri { + type Error = errors::IdUriError; + + fn try_from(value: &[u8]) -> Result { + let kid_str = String::from_utf8(value.to_vec())?; + IdUri::from_str(&kid_str) + } +} + +#[cfg(test)] +mod tests { + use ed25519_dalek::SigningKey; + use rand::rngs::OsRng; + + use super::IdUri; + + const ID_URI_TEST_VECTOR: [&str; 9] = [ + "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "user@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "user:1735689600@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + ":1735689600@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3", + "id.catalyst://preview.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt", + "id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1", + "id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt", + ]; + + #[test] + /// Tests that deserialization and re-serialization round trip correctly + fn test_id_uri_from_str() { + for id_string in ID_URI_TEST_VECTOR { + let id = id_string.parse::().unwrap(); + assert_eq!(format!("{id}"), id_string); + } + } + + #[test] + /// Tests that a short form of a long ID is the same as a short deserialized ID + fn test_short_id() { + let test_uri = "id.catalyst://user:1735689600@preview.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt"; + let expected_id = "preview.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE"; + + let uri_id = test_uri.parse::().unwrap(); + let short_id = expected_id.parse::().unwrap(); + + assert_eq!(uri_id.as_short_id(), short_id); + } + + #[ignore] + #[test] + fn gen_pk() { + let mut csprng = OsRng; + let signing_key: SigningKey = SigningKey::generate(&mut csprng); + let vk = signing_key.verifying_key(); + let encoded_vk = base64_url::encode(vk.as_bytes()); + assert_eq!(encoded_vk, "1234"); + } +} diff --git a/rust/catalyst-types/src/kid_uri/role_index.rs b/rust/catalyst-types/src/id_uri/role_index.rs similarity index 76% rename from rust/catalyst-types/src/kid_uri/role_index.rs rename to rust/catalyst-types/src/id_uri/role_index.rs index 24b795273e..d45071c4f2 100644 --- a/rust/catalyst-types/src/kid_uri/role_index.rs +++ b/rust/catalyst-types/src/id_uri/role_index.rs @@ -23,6 +23,23 @@ pub enum RoleIndexError { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RoleIndex(u16); +impl RoleIndex { + /// Default Role Index + pub const DEFAULT: RoleIndex = RoleIndex(0); + + /// Is the `RoleIndex` the default value + #[must_use] + pub fn is_default(self) -> bool { + self == Self::DEFAULT + } +} + +impl Default for RoleIndex { + fn default() -> Self { + Self::DEFAULT + } +} + impl From for RoleIndex { fn from(value: u16) -> Self { Self(value) diff --git a/rust/catalyst-types/src/kid_uri/mod.rs b/rust/catalyst-types/src/kid_uri/mod.rs deleted file mode 100644 index d58dbac92f..0000000000 --- a/rust/catalyst-types/src/kid_uri/mod.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! COSE Signature Protected Header `kid`. - -// cspell: words userinfo rngs Fftx csprng - -mod errors; -mod key_rotation; -mod role_index; - -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -use ed25519_dalek::VerifyingKey; -use fluent_uri::{ - component::Scheme, - encoding::{ - encoder::{Fragment, Path}, - EStr, - }, - Uri, -}; -use key_rotation::KeyRotation; -use role_index::RoleIndex; - -/// Catalyst Signed Document Key ID -/// -/// Key ID associated with a `COSE` Signature that is structured as a Universal Resource -/// Identifier (`URI`). -#[derive(Debug, Clone)] -#[allow(clippy::module_name_repetitions)] -pub struct KidUri { - /// Network - network: String, - /// Sub Network - subnet: Option, - /// Role0 Public Key. - role0_pk: VerifyingKey, - /// User Role specified for the current document. - role: RoleIndex, - /// Role Key Rotation count - rotation: KeyRotation, - /// Indicates whether this key is an encryption key. - /// - `true`: The key is used for encryption. - /// - `false`: The key is used for signing (signature key). - encryption: bool, -} - -impl KidUri { - /// Encryption Key Identifier Fragment - const ENCRYPTION_FRAGMENT: &EStr = EStr::new_or_panic("encrypt"); - /// URI scheme for Catalyst - const SCHEME: &Scheme = Scheme::new_or_panic("kid.catalyst-rbac"); - - /// Get the network the `KidUri` is referencing the registration to. - #[must_use] - pub fn network(&self) -> (String, Option) { - (self.network.clone(), self.subnet.clone()) - } - - /// Is the key a signature type key. - #[must_use] - pub fn is_signature_key(&self) -> bool { - !self.encryption - } - - /// Is the key an encryption type key. - #[must_use] - pub fn is_encryption_key(&self) -> bool { - self.encryption - } - - /// Get the Initial Role 0 Key of the registration - #[must_use] - pub fn role0_pk(&self) -> VerifyingKey { - self.role0_pk - } - - /// Get the role index and its rotation count - #[must_use] - pub fn role_and_rotation(&self) -> (RoleIndex, KeyRotation) { - (self.role, self.rotation) - } -} - -impl KidUri { - /// Create a new `KidUri` for a Signing Key - fn new( - network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, - rotation: KeyRotation, - ) -> Self { - Self { - network: network.to_string(), - subnet: subnet.map(str::to_string), - role0_pk, - role, - rotation, - encryption: false, - } - } - - /// Create a new `KidUri` for an Encryption Key - fn new_encryption( - network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, - rotation: KeyRotation, - ) -> Self { - let mut kid = Self::new(network, subnet, role0_pk, role, rotation); - kid.encryption = true; - kid - } -} - -impl FromStr for KidUri { - type Err = errors::KidUriError; - - fn from_str(s: &str) -> Result { - let uri = Uri::parse(s)?; - - // Check if its the correct scheme. - if uri.scheme() != KidUri::SCHEME { - return Err(errors::KidUriError::InvalidScheme); - } - - // Decode the network and subnet - let auth = uri - .authority() - .ok_or(errors::KidUriError::NoDefinedNetwork)?; - let network = auth.host(); - let subnet = auth.userinfo().map(std::string::ToString::to_string); - - let path: Vec<&EStr> = uri.path().split('/').collect(); - - // Can ONLY have 3 path components, no more and no less - // Less than 3 handled by errors below (4 because of leading `/` in path). - if path.len() > 4 { - return Err(errors::KidUriError::InvalidPath); - }; - - // Decode and validate the Role0 Public key from the path - let encoded_role0_key = path.get(1).ok_or(errors::KidUriError::InvalidRole0Key)?; - let decoded_role0_key = - base64_url::decode(encoded_role0_key.decode().into_string_lossy().as_ref())?; - let role0_pk = crate::conversion::vkey_from_bytes(&decoded_role0_key) - .or(Err(errors::KidUriError::InvalidRole0Key))?; - - // Decode and validate the Role Index from the path. - let encoded_role_index = path.get(2).ok_or(errors::KidUriError::InvalidRole)?; - let decoded_role_index = encoded_role_index.decode().into_string_lossy(); - let role_index = decoded_role_index.parse::()?; - - // Decode and validate the Rotation Value from the path. - let encoded_rotation = path.get(3).ok_or(errors::KidUriError::InvalidRotation)?; - let decoded_rotation = encoded_rotation.decode().into_string_lossy(); - let rotation = decoded_rotation.parse::()?; - - let kid = { - if uri.has_fragment() { - if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { - Self::new_encryption(network, subnet.as_deref(), role0_pk, role_index, rotation) - } else { - return Err(errors::KidUriError::InvalidEncryptionKeyFragment); - } - } else { - Self::new(network, subnet.as_deref(), role0_pk, role_index, rotation) - } - }; - - Ok(kid) - } -} - -impl Display for KidUri { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}://", Self::SCHEME.as_str())?; - if let Some(subnet) = &self.subnet { - write!(f, "{subnet}@")?; - } - write!( - f, - "{}/{}/{}/{}", - self.network, - base64_url::encode(self.role0_pk.as_bytes()), - self.role, - self.rotation - )?; - if self.encryption { - write!(f, "#{}", Self::ENCRYPTION_FRAGMENT)?; - } - Ok(()) - } -} - -impl TryFrom<&[u8]> for KidUri { - type Error = errors::KidUriError; - - fn try_from(value: &[u8]) -> Result { - let kid_str = String::from_utf8_lossy(value); - KidUri::from_str(&kid_str) - } -} - -#[cfg(test)] -mod tests { - use ed25519_dalek::SigningKey; - use rand::rngs::OsRng; - - use super::KidUri; - - const KID_TEST_VECTOR: [&str; 5] = [ - "kid.catalyst-rbac://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0", - "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3", - "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt", - "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1", - "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt" - ]; - - #[test] - fn test_kid_uri_from_str() { - for kid_string in KID_TEST_VECTOR { - let kid = kid_string.parse::().unwrap(); - assert_eq!(format!("{kid}"), kid_string); - } - } - - #[ignore] - #[test] - fn gen_pk() { - let mut csprng = OsRng; - let signing_key: SigningKey = SigningKey::generate(&mut csprng); - let vk = signing_key.verifying_key(); - let encoded_vk = base64_url::encode(vk.as_bytes()); - assert_eq!(encoded_vk, "1234"); - } -} diff --git a/rust/catalyst-types/src/lib.rs b/rust/catalyst-types/src/lib.rs index 324257bd5d..4802ce378a 100644 --- a/rust/catalyst-types/src/lib.rs +++ b/rust/catalyst-types/src/lib.rs @@ -2,7 +2,7 @@ pub mod conversion; pub mod hashes; -pub mod kid_uri; +pub mod id_uri; pub mod mmap_file; pub mod problem_report; pub mod uuid; diff --git a/rust/catalyst-types/tests/mod.rs b/rust/catalyst-types/tests/mod.rs index ab9a597938..1c9fd38856 100644 --- a/rust/catalyst-types/tests/mod.rs +++ b/rust/catalyst-types/tests/mod.rs @@ -12,7 +12,7 @@ fn test_type_usage() { type D = catalyst_types::uuid::UuidV4; type E = catalyst_types::uuid::UuidV7; - type F = catalyst_types::kid_uri::KidUri; + type F = catalyst_types::id_uri::IdUri; let bytes: [u8; 32] = [0; 32]; let _ = catalyst_types::hashes::Blake2bHash::from(bytes);