diff --git a/crypto/crypto.test.ts b/crypto/crypto.test.ts index ca036f1..2e9c807 100644 --- a/crypto/crypto.test.ts +++ b/crypto/crypto.test.ts @@ -39,6 +39,7 @@ Deno.test("sign & verify payload with wrong key order but should still work", as sig: signedPayload.sig, signee: { payload: { baz: 123, foo: "bar" }, + act_key: signedPayload.signee.act_key, id_key: signedPayload.signee.id_key, nonce: signedPayload.signee.nonce, timestamp: signedPayload.signee.timestamp, diff --git a/crypto/keypair.ts b/crypto/keypair.ts index 5290094..3784819 100644 --- a/crypto/keypair.ts +++ b/crypto/keypair.ts @@ -80,10 +80,8 @@ export class BlahKeyPair { payload, timestamp, id_key: identityKeyId ?? this.id, + act_key: this.id, }; - if (identityKeyId) { - signee.act_key = this.id; - } const signeeBytes = new TextEncoder().encode(canonicalize(signee)); diff --git a/crypto/publicKey.ts b/crypto/publicKey.ts index b69afc7..4e3fb11 100644 --- a/crypto/publicKey.ts +++ b/crypto/publicKey.ts @@ -46,10 +46,10 @@ export class BlahPublicKey { async verifyPayload

(signedPayload: BlahSignedPayload

): Promise

{ const { sig, signee } = signedPayload; - const signingKey = signee.act_key ?? signee.id_key; + const signingKey = signee.act_key; if (signingKey !== this.id) { throw new Error( - `Payload is not signed by this identity. Was signed by ${signingKey}.`, + `Payload is not signed by this public key. Was signed by ${signingKey}.`, ); } const signeeBytes = new TextEncoder().encode(canonicalize(signee)); diff --git a/crypto/signedPayload.ts b/crypto/signedPayload.ts index 22ec36e..1fdc43e 100644 --- a/crypto/signedPayload.ts +++ b/crypto/signedPayload.ts @@ -6,7 +6,7 @@ export function blahPayloadSigneeSchemaOf

(schema: P) { payload: schema, timestamp: z.number().int(), id_key: z.string(), - act_key: z.string().optional(), + act_key: z.string(), }); } @@ -22,7 +22,7 @@ export type BlahPayloadSignee

= { payload: P; timestamp: number; id_key: string; - act_key?: string; + act_key: string; }; export type BlahSignedPayload

= { diff --git a/deno.json b/deno.json index 80537d1..e572fa6 100644 --- a/deno.json +++ b/deno.json @@ -17,5 +17,8 @@ "publish": { "include": ["LICENSE", "README.md", "crypto", "identity"], "exclude": ["**/*.test.ts"] + }, + "test": { + "exclude": ["npm/"] } } diff --git a/identity/identity.ts b/identity/identity.ts index 1dfb49f..387c771 100644 --- a/identity/identity.ts +++ b/identity/identity.ts @@ -1,9 +1,100 @@ -import type { BlahSignedPayload } from "../crypto/mod.ts"; +import { + BlahKeyPair, + BlahPublicKey, + BlahSignedPayload, +} from "../crypto/mod.ts"; import type { BlahActKeyRecord } from "./actKey.ts"; +import { blahIdentityFileSchema } from "./identityFile.ts"; import type { BlahProfile } from "./profile.ts"; -export class BlahIdentity { - idKeyId: string; - actKeys: BlahSignedPayload[]; - profile: BlahSignedPayload; +type ActKey = { + raw: BlahSignedPayload; + key: BlahPublicKey | BlahKeyPair; + expiresAt: Date; + sigValid: boolean; +}; + +async function constructActKeyFromRaw( + raw: BlahSignedPayload, + idKey: BlahPublicKey | BlahKeyPair, +): Promise { + const publicKey = idKey instanceof BlahKeyPair ? idKey.publicKey : idKey; + let sigValid = false; + try { + 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 }; +} + +export class BlahIdentity { + idKey: BlahPublicKey | BlahKeyPair; + actKeys: ActKey[]; + rawProfile: BlahSignedPayload; + profileSigValid: boolean; + + private constructor( + idKey: BlahPublicKey | BlahKeyPair, + actKeys: ActKey[], + rawProfile: BlahSignedPayload, + profileSigValid: boolean, + ) { + this.idKey = idKey; + this.actKeys = actKeys; + this.rawProfile = rawProfile; + this.profileSigValid = profileSigValid; + } + + get profile(): BlahProfile { + return this.rawProfile.signee.payload; + } + + static async fromIdentityFile( + identityFile: unknown, + idKeyPair?: BlahKeyPair, + actingKeyPair?: BlahKeyPair, + ): Promise { + let identityFileJson = identityFile; + if (typeof identityFile === "string") { + identityFileJson = JSON.parse(identityFile); + } + const { id_key, act_keys, profile } = blahIdentityFileSchema.parse( + identityFileJson, + ); + + const idKey = idKeyPair ?? await BlahPublicKey.fromID(id_key); + if (idKey.id !== id_key) { + throw new Error("ID key pair does not match ID key in identity file."); + } + + const actKeys: ActKey[] = await Promise.all(act_keys.map(async (raw) => { + const actKey = await constructActKeyFromRaw(raw, idKey); + if (actingKeyPair?.id === actKey.key.id) { + actKey.key = actingKeyPair; + } + return actKey; + })); + + const rawProfile = profile; + const profileSigningKey = await BlahPublicKey.fromID( + rawProfile.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 { + profileSigningKey.verifyPayload(rawProfile); + profileSigValid = true; + } catch { + profileSigValid = false; + } + + return new BlahIdentity(idKey, actKeys, rawProfile, profileSigValid); + } }