From 79efc82800a7e6aca3e8516bbb4866bd502e2f36 Mon Sep 17 00:00:00 2001 From: Dominik Toton <166132265+dtscalac@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:45:03 +0200 Subject: [PATCH] feat: add catv1 auth token generator (#671) * feat: add auth token generator * docs: markdown format * chore: lint issues --- .../lib/catalyst_cardano_serialization.dart | 1 + .../lib/src/rbac/auth_token.dart | 71 ++++++++++++++ .../pubspec.yaml | 1 + .../test/rbac/auth_token_test.dart | 93 +++++++++++++++++++ melos.yaml | 1 + 5 files changed, 167 insertions(+) create mode 100644 catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart create mode 100644 catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/auth_token_test.dart diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart index 7c7607b504..cabec03a94 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart @@ -6,6 +6,7 @@ export 'src/cip95/drep.dart'; export 'src/exceptions.dart'; export 'src/fees.dart'; export 'src/hashes.dart'; +export 'src/rbac/auth_token.dart'; export 'src/rbac/registration_data.dart'; export 'src/rbac/x509_metadata_envelope.dart'; export 'src/signature.dart'; diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart new file mode 100644 index 0000000000..14f1a33ecd --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:cbor/cbor.dart'; +import 'package:ulid/ulid.dart'; + +/// The Authentication Token is based loosely on JWT. +/// It consists of an Authentication Header attached to every authenticated +/// request, and an encoded signed. +/// +/// This token can be attached to either individual HTTP requests, +/// or to the beginning of a web socket connection. +/// +/// The authentication header is in the format: +/// +/// ```http +/// Authorization: Bearer catv1. +/// ``` +/// +/// ### Encoded Binary Token Format +/// +/// The Encoded Binary Token is a [CBOR sequence] that consists of 3 fields. +/// +/// * `kid` : The key identifier. +/// * `ulid` : A ULID which defines when the token was issued, +/// and a random nonce. +/// * `signature` : The signature over the `kid` and `ulid` fields. +final class AuthToken { + /// The token prefix which distinguishes this auth token from other + /// auth tokens and allows version via the v{} part. + static const String prefix = 'catv1'; + + /// Prevent creating instances. + const AuthToken._(); + + /// Generates a new auth token at a given [timestamp]. + /// + /// * The [kid] in most cases is going to be a [CertificateHash] + /// of the [privateKey] certificate. + /// * The [privateKey] must correspond to the [kid] specified. + /// * The [timestamp] is a [DateTime] when a given token has been generated. + static Future generate({ + required CertificateHash kid, + required Ed25519PrivateKey privateKey, + required DateTime timestamp, + }) async { + final ulid = CborBytes( + Ulid(millis: timestamp.millisecondsSinceEpoch).toBytes(), + ); + + final toBeSigned = CborBytes( + cbor.encode( + CborList([ + kid.toCbor(), + ulid, + ]), + ), + ); + + final signature = await privateKey.sign(cbor.encode(toBeSigned)); + + final cborToken = [ + ...cbor.encode(kid.toCbor()), + ...cbor.encode(ulid), + ...cbor.encode(signature.toCbor()), + ]; + + final base64Token = base64Encode(cborToken); + return '$prefix.$base64Token'; + } +} diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml b/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml index 62da9af3c0..6e39412d6d 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml +++ b/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: cryptography: ^2.7.0 equatable: ^2.0.5 pinenacl: ^0.6.0 + ulid: ^2.0.0 dev_dependencies: catalyst_analysis: diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/auth_token_test.dart b/catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/auth_token_test.dart new file mode 100644 index 0000000000..e89c670e11 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/auth_token_test.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:cbor/cbor.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:test/test.dart'; +import 'package:ulid/ulid.dart'; + +// The certificate provided in the request +final _c509Cert = C509Certificate.fromHex( + ''' +8B004301F50D6B524643207465737420 +43411A63B0CD001A6955B90047010123 +456789AB01582102B1216AB96E5B3B33 +40F5BDF02E693F16213A04525ED44450 +B1019C2DFD3838AB010058406FC90301 +5259A38C0800A3D0B2969CA21977E8ED +6EC344964D4E1C6B37C8FB541274C3BB +81B2F53073C5F101A5AC2A92886583B6 +A2679B6E682D2A26945ED0B2 +''' + .replaceAll('\n', ''), +); + +void main() { + group(AuthToken, () { + final kid = CertificateHash.fromC509Certificate(_c509Cert); + final privateKey = Ed25519PrivateKey.seeded(0); + final timestamp = DateTime.utc(2023); + + test('Generate AuthToken', () async { + final token = await AuthToken.generate( + kid: kid, + privateKey: privateKey, + timestamp: timestamp, + ); + + expect(token, startsWith('${AuthToken.prefix}.')); + + // Decode the base64 token part + final parts = token.split('.'); + expect(parts.length, 2); + + final base64Token = parts[1]; + final decodedToken = base64Decode(base64Token); + + final decodedTokenAsArray = [ + 0x83, + ...decodedToken, + ]; + + // Decode the CBOR token + final decodedCbor = cbor.decode(decodedTokenAsArray) as CborList; + expect(decodedCbor.length, 3); + + final decodedKid = decodedCbor[0] as CborBytes; + final decodedUlid = decodedCbor[1] as CborBytes; + final decodedSignature = decodedCbor[2] as CborBytes; + + expect(decodedKid.bytes, (kid.toCbor() as CborBytes).bytes); + expect( + Ulid.fromBytes(decodedUlid.bytes).toMillis(), + timestamp.millisecondsSinceEpoch, + ); + + // Verify the signature + final toBeSigned = CborBytes( + cbor.encode( + CborList([ + kid.toCbor(), + decodedUlid, + ]), + ), + ); + + final toBeSignedEncoded = cbor.encode(toBeSigned); + final publicKey = await privateKey.derivePublicKey(); + + final isValid = await Ed25519().verify( + toBeSignedEncoded, + signature: Signature( + decodedSignature.bytes, + publicKey: SimplePublicKey( + publicKey.bytes, + type: KeyPairType.ed25519, + ), + ), + ); + + expect(isValid, isTrue); + }); + }); +} diff --git a/melos.yaml b/melos.yaml index 067d6ca79a..e514850e62 100644 --- a/melos.yaml +++ b/melos.yaml @@ -33,6 +33,7 @@ command: cbor: ^6.2.0 convert: ^3.1.1 pinenacl: ^0.6.0 + ulid: ^2.0.0 dev_dependencies: test: ^1.24.9 build_runner: ^2.3.3