diff --git a/crypto/keypair.ts b/crypto/keypair.ts
index e2e076f..2f176e3 100644
--- a/crypto/keypair.ts
+++ b/crypto/keypair.ts
@@ -42,6 +42,10 @@ export class BlahKeyPair {
this.internalPrivateKey = privateKey;
}
+ async verifyPayload
(signedPayload: BlahSignedPayload
): Promise
{
+ return await this.internalPublicKey.verifyPayload(signedPayload);
+ }
+
static async generate(extractable: boolean = true): Promise {
const { publicKey, privateKey } = await crypto.subtle.generateKey(
"Ed25519",
diff --git a/identity/actKey.ts b/identity/actKey.ts
index 9314b85..bdad622 100644
--- a/identity/actKey.ts
+++ b/identity/actKey.ts
@@ -1,4 +1,7 @@
import z from "zod";
+import { BlahPublicKey } from "../crypto/publicKey.ts";
+import { BlahKeyPair } from "../crypto/keypair.ts";
+import type { BlahSignedPayload } from "../crypto/signedPayload.ts";
export const blahActKeyRecordSchema = z.object({
typ: z.literal("user_act_key"),
@@ -13,3 +16,156 @@ export type BlahActKeyRecord = {
expire_time: number;
comment: string;
};
+
+export type ActKeyUpdate = {
+ expiresAt?: Date;
+ comment?: string;
+};
+
+export class BlahActKey {
+ private internalKey: BlahPublicKey | BlahKeyPair;
+ private internalExpiresAt: Date;
+ private internalComment: string;
+ private internalIdKeyPublic: BlahPublicKey;
+ private internalSigValid: boolean;
+ private internalSignedRecord: BlahSignedPayload;
+
+ private constructor(
+ key: BlahPublicKey | BlahKeyPair,
+ idKeyPublic: BlahPublicKey,
+ fullConfig: Required,
+ sigValid: boolean,
+ signedRecord: BlahSignedPayload,
+ ) {
+ this.internalKey = key;
+ this.internalIdKeyPublic = idKeyPublic;
+ this.internalExpiresAt = fullConfig.expiresAt;
+ this.internalComment = fullConfig.comment;
+ this.internalSigValid = sigValid;
+ this.internalSignedRecord = signedRecord;
+ }
+
+ static async fromSignedRecord(
+ raw: BlahSignedPayload,
+ idKeyPublic: BlahPublicKey,
+ keypair?: BlahKeyPair,
+ ): Promise {
+ let record: BlahActKeyRecord;
+ let sigValid = false;
+ try {
+ record = await idKeyPublic.verifyPayload(raw);
+ sigValid = true;
+ } catch {
+ record = raw.signee.payload;
+ sigValid = false;
+ }
+
+ const key: BlahPublicKey | BlahKeyPair = keypair ??
+ await BlahPublicKey.fromID(record.act_key);
+ const fullConfig: Required = {
+ expiresAt: new Date(record.expire_time * 1000),
+ comment: record.comment,
+ };
+
+ return new BlahActKey(key, idKeyPublic, fullConfig, sigValid, raw);
+ }
+
+ static async create(
+ key: BlahPublicKey | BlahKeyPair,
+ idKeyPair: BlahKeyPair,
+ config?: ActKeyUpdate,
+ ): Promise {
+ const fullConfig: Required = {
+ expiresAt: new Date(Date.now() + 365 * 24 * 3600 * 1000),
+ comment: "",
+ ...config,
+ };
+ const record: BlahActKeyRecord = {
+ typ: "user_act_key",
+ act_key: key.id,
+ expire_time: Math.floor(fullConfig.expiresAt.getTime() / 1000),
+ comment: fullConfig.comment,
+ };
+
+ const signedRecord = await idKeyPair.signPayload(record);
+
+ return new BlahActKey(
+ key,
+ idKeyPair.publicKey,
+ fullConfig,
+ true,
+ signedRecord,
+ );
+ }
+
+ get isExpired(): boolean {
+ return new Date() > this.internalExpiresAt;
+ }
+
+ get isSigValid(): boolean {
+ return this.internalSigValid;
+ }
+
+ get comment(): string {
+ return this.internalComment;
+ }
+
+ get canSign(): boolean {
+ return this.internalKey instanceof BlahKeyPair;
+ }
+
+ get publicKey(): BlahPublicKey {
+ if (this.internalKey instanceof BlahKeyPair) {
+ return this.internalKey.publicKey;
+ } else {
+ return this.internalKey;
+ }
+ }
+
+ async signPayload(
+ payload: P,
+ date: Date = new Date(),
+ ): Promise> {
+ if (!this.canSign) throw new Error("Cannot sign without a private key");
+
+ return await (this.internalKey as BlahKeyPair).signPayload(
+ payload,
+ date,
+ this.internalIdKeyPublic.id,
+ );
+ }
+
+ async verifyPayload(
+ payload: BlahSignedPayload
,
+ ): Promise
{
+ if (payload.signee.id_key !== this.internalIdKeyPublic.id) {
+ throw new Error("Payload signed with a different ID key");
+ }
+ if (new Date(payload.signee.timestamp * 1000) > this.internalExpiresAt) {
+ throw new Error("Key was expired at the time of signing");
+ }
+ return await this.internalKey.verifyPayload(payload);
+ }
+
+ async update(update: ActKeyUpdate, idKeyPair: BlahKeyPair): Promise {
+ if (update.expiresAt) this.internalExpiresAt = update.expiresAt;
+ if (update.comment) this.internalComment = update.comment;
+
+ this.internalSignedRecord = await idKeyPair.signPayload({
+ typ: "user_act_key",
+ act_key: this.internalKey.id,
+ expire_time: Math.floor(this.internalExpiresAt.getTime() / 1000),
+ comment: this.internalComment,
+ });
+ this.internalSigValid = true;
+ }
+
+ toSignedRecord(): BlahSignedPayload {
+ return this.internalSignedRecord;
+ }
+
+ setKeyPair(keypair: BlahKeyPair) {
+ if (this.internalKey.id !== keypair.id) throw new Error("Key ID mismatch");
+ this.internalKey = keypair;
+ }
+}
diff --git a/identity/identity.test.ts b/identity/identity.test.ts
index 39349c5..1aeacc9 100644
--- a/identity/identity.test.ts
+++ b/identity/identity.test.ts
@@ -76,7 +76,7 @@ Deno.test("identity file act key sigs are properly verfied", async () => {
.fromIdentityFile(
identityFileWithActKeyInvalidActKeySig,
);
- expect(identityWithActKeyInvalidActKeySig.actKeys[0].sigValid).toBe(false);
+ expect(identityWithActKeyInvalidActKeySig.actKeys[0].isSigValid).toBe(false);
});
Deno.test("add a second act key", async () => {
@@ -105,6 +105,12 @@ Deno.test("update first act key", async () => {
expect(record.comment).toBe("test2");
});
+Deno.test("act key properly expires", async () => {
+ expect(identity.actKeys[0].isExpired).toBe(false);
+ await identity.updateActKey(actKeyPair.id, { expiresAt: new Date(10000) });
+ expect(identity.actKeys[0].isExpired).toBe(true);
+});
+
Deno.test("update profile", async () => {
const newProfile: BlahProfile = {
typ: "profile",
diff --git a/identity/identity.ts b/identity/identity.ts
index 5de241c..bc8e573 100644
--- a/identity/identity.ts
+++ b/identity/identity.ts
@@ -3,87 +3,20 @@ import {
BlahPublicKey,
type BlahSignedPayload,
} from "../crypto/mod.ts";
-import { type BlahActKeyRecord, blahActKeyRecordSchema } from "./actKey.ts";
+import { type ActKeyUpdate, BlahActKey } from "./actKey.ts";
import { blahIdentityFileSchema } from "./identityFile.ts";
import type { BlahIdentityFile } from "./mod.ts";
import type { BlahProfile } from "./profile.ts";
-type InternalActKey = {
- raw: BlahSignedPayload;
- key: BlahPublicKey | BlahKeyPair;
- expiresAt: Date;
- sigValid: boolean;
-};
-
-type ActKey = {
- publicKey: BlahPublicKey;
- expiresAt: Date;
- sigValid: boolean;
- comment: string;
-};
-
-type ActKeyConfig = Partial>;
-
-async function constructActKeyFromRaw(
- raw: BlahSignedPayload,
- idKey: BlahPublicKey | BlahKeyPair,
-): Promise {
- const publicKey = idKey instanceof BlahKeyPair ? idKey.publicKey : idKey;
- let sigValid = false;
- try {
- await publicKey.verifyPayload(raw);
- sigValid = true;
- } catch {
- sigValid = false;
- }
-
- const key = await BlahPublicKey.fromID(raw.signee.payload.act_key);
- const expiresAt = new Date(raw.signee.payload.expire_time * 1000);
- return { raw, key, expiresAt, sigValid };
-}
-
-async function constructInternalActKey(
- idKeyPair: BlahKeyPair,
- key: BlahPublicKey | BlahKeyPair,
- config?: ActKeyConfig,
-): Promise {
- const actKey: ActKey = {
- publicKey: key instanceof BlahKeyPair ? key.publicKey : key,
- expiresAt: new Date(Date.now() + 365 * 24 * 3600 * 1000),
- sigValid: true,
- comment: "",
- ...config,
- };
-
- const rawRecord = await idKeyPair
- .signPayload(blahActKeyRecordSchema.parse(
- {
- typ: "user_act_key",
- expire_time: Math.floor(actKey.expiresAt.getTime() / 1000),
- comment: actKey.comment,
- act_key: actKey.publicKey.id,
- } satisfies BlahActKeyRecord,
- ));
-
- const internalActKey: InternalActKey = {
- raw: rawRecord,
- key,
- expiresAt: actKey.expiresAt,
- sigValid: true,
- };
-
- return internalActKey;
-}
-
export class BlahIdentity {
private internalIdKey: BlahPublicKey | BlahKeyPair;
- private internalActKeys: InternalActKey[];
+ private internalActKeys: BlahActKey[];
private rawProfile: BlahSignedPayload;
private internalProfileSigValid: boolean;
private constructor(
internalIdKey: BlahPublicKey | BlahKeyPair,
- internalActKeys: InternalActKey[],
+ internalActKeys: BlahActKey[],
rawProfile: BlahSignedPayload,
internalProfileSigValid: boolean,
) {
@@ -107,13 +40,8 @@ export class BlahIdentity {
: this.internalIdKey;
}
- get actKeys(): ActKey[] {
- return this.internalActKeys.map(({ key, expiresAt, sigValid, raw }) => ({
- publicKey: key instanceof BlahKeyPair ? key.publicKey : key,
- expiresAt,
- sigValid,
- comment: raw.signee.payload.comment,
- }));
+ get actKeys(): BlahActKey[] {
+ return this.internalActKeys;
}
static async fromIdentityFile(
@@ -133,28 +61,31 @@ export class BlahIdentity {
if (idKey.id !== id_key) {
throw new Error("ID key pair does not match ID key in identity file.");
}
+ const idKeyPublic = idKey instanceof BlahKeyPair ? idKey.publicKey : idKey;
- const actKeys: InternalActKey[] = await Promise.all(
+ const actKeys: BlahActKey[] = await Promise.all(
act_keys.map(async (raw) => {
- const actKey = await constructActKeyFromRaw(raw, idKey);
- if (actingKeyPair?.id === actKey.key.id) actKey.key = actingKeyPair;
+ const actKey = await BlahActKey.fromSignedRecord(raw, idKeyPublic);
+ if (actingKeyPair?.id === actKey.publicKey.id) {
+ actKey.setKeyPair(actingKeyPair);
+ }
return actKey;
}),
);
const rawProfile = profile;
- const profileSigningKey = await BlahPublicKey.fromID(
- rawProfile.signee.act_key,
+ const profileSigningKey = actKeys.find((k) =>
+ k.publicKey.id === profile.signee.act_key
);
- if (actKeys.findIndex((k) => k.key.id === profileSigningKey.id) === -1) {
- throw new Error("Profile is not signed by any of the act keys.");
- }
+
let profileSigValid = false;
- try {
- await profileSigningKey.verifyPayload(rawProfile);
- profileSigValid = true;
- } catch {
- profileSigValid = false;
+ if (profileSigningKey) {
+ try {
+ await profileSigningKey.verifyPayload(rawProfile);
+ profileSigValid = true;
+ } catch {
+ profileSigValid = false;
+ }
}
return new BlahIdentity(idKey, actKeys, rawProfile, profileSigValid);
@@ -164,82 +95,57 @@ export class BlahIdentity {
idKeyPair: BlahKeyPair,
firstActKey: BlahKeyPair,
profile: BlahProfile,
- firstActKeyConfig?: ActKeyConfig,
+ firstActKeyConfig?: ActKeyUpdate,
): Promise {
- const internalActKey = await constructInternalActKey(
- idKeyPair,
+ const actKey = await BlahActKey.create(
firstActKey,
+ idKeyPair,
firstActKeyConfig,
);
- const profileRecord: BlahSignedPayload = await firstActKey
- .signPayload(profile);
+ const profileRecord = await firstActKey.signPayload(profile);
- return new BlahIdentity(
- idKeyPair,
- [internalActKey],
- profileRecord,
- true,
- );
+ return new BlahIdentity(idKeyPair, [actKey], profileRecord, true);
}
generateIdentityFile(): BlahIdentityFile {
return blahIdentityFileSchema.parse(
{
id_key: this.idPublicKey.id,
- act_keys: this.internalActKeys.map((k) => (k.raw)),
+ act_keys: this.internalActKeys.map((k) => k.toSignedRecord()),
profile: this.rawProfile,
} satisfies BlahIdentityFile,
);
}
- async addActKey(actKey: BlahKeyPair | BlahPublicKey, config?: ActKeyConfig) {
+ async addActKey(actKey: BlahKeyPair | BlahPublicKey, config?: ActKeyUpdate) {
if (this.internalIdKey instanceof BlahPublicKey) {
throw new Error("Cannot add act key to identity without ID key pair.");
}
- const internalActKey = await constructInternalActKey(
- this.internalIdKey,
- actKey,
- config,
- );
-
- this.internalActKeys.push(internalActKey);
+ const key = await BlahActKey.create(actKey, this.internalIdKey, config);
+ this.internalActKeys.push(key);
}
- async updateActKey(
- keyId: string,
- config: ActKeyConfig,
- ) {
+ async updateActKey(id: string, update: ActKeyUpdate) {
if (this.internalIdKey instanceof BlahPublicKey) {
throw new Error("Cannot update act key in identity without ID key pair.");
}
- const actKeyIndex = this.internalActKeys.findIndex(
- (k) => k.key.id === keyId,
- );
- if (actKeyIndex === -1) {
+ const key = this.internalActKeys.find((k) => k.publicKey.id === id);
+ if (!key) {
throw new Error("Act key not found in identity.");
}
-
- this.internalActKeys[actKeyIndex] = await constructInternalActKey(
- this.internalIdKey,
- this.internalActKeys[actKeyIndex].key,
- config,
- );
+ await key.update(update, this.internalIdKey);
}
async updateProfile(profile: BlahProfile) {
- const signingActKey = this.internalActKeys.find((k) =>
- k.key instanceof BlahKeyPair
- );
+ const signingActKey = this.internalActKeys.find((k) => k.canSign);
if (!signingActKey) {
throw new Error("No act key to sign profile with.");
}
- this.rawProfile = await (signingActKey.key as BlahKeyPair).signPayload(
- profile,
- );
+ this.rawProfile = await signingActKey.signPayload(profile);
this.internalProfileSigValid = true;
}
}