Skip to content

Commit

Permalink
feat: add catv1 auth token generator (#671)
Browse files Browse the repository at this point in the history
* feat: add auth token generator

* docs: markdown format

* chore: lint issues
  • Loading branch information
dtscalac authored Aug 9, 2024
1 parent e014bfc commit 79efc82
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 token>
/// ```
///
/// ### 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<String> 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';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
1 change: 1 addition & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 79efc82

Please sign in to comment.