Skip to content

Commit 606f73c

Browse files
authored
[vc] add verifiable presentation creation and signing (#6)
1 parent 4031271 commit 606f73c

File tree

11 files changed

+304
-28
lines changed

11 files changed

+304
-28
lines changed

.changeset/thirty-aliens-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@agentcommercekit/vc": minor
3+
---
4+
5+
add verifiable presentation creation and signing
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, it } from "vitest"
2+
import { createPresentation } from "./create-presentation"
3+
import type { Verifiable, W3CCredential } from "./types"
4+
5+
describe("createPresentation", () => {
6+
const mockHolder = "did:example:holder"
7+
8+
const mockCredential: Verifiable<W3CCredential> = {
9+
"@context": ["https://www.w3.org/2018/credentials/v1"],
10+
type: ["VerifiableCredential"],
11+
issuer: { id: "did:example:issuer" },
12+
credentialSubject: { id: "did:example:subject" },
13+
issuanceDate: new Date().toISOString(),
14+
proof: {
15+
type: "Ed25519Signature2018"
16+
}
17+
}
18+
19+
it("should create a basic presentation with required fields", () => {
20+
const presentation = createPresentation({
21+
credentials: [mockCredential],
22+
holder: mockHolder
23+
})
24+
25+
expect(presentation).toEqual({
26+
"@context": ["https://www.w3.org/2018/credentials/v1"],
27+
type: ["VerifiablePresentation"],
28+
holder: mockHolder,
29+
verifiableCredential: [mockCredential]
30+
})
31+
})
32+
33+
it("should handle multiple credentials", () => {
34+
const secondCredential = {
35+
...mockCredential,
36+
credentialSubject: { id: "did:example:subject2" }
37+
}
38+
const presentation = createPresentation({
39+
credentials: [mockCredential, secondCredential],
40+
holder: mockHolder
41+
})
42+
43+
expect(presentation.verifiableCredential).toEqual([
44+
mockCredential,
45+
secondCredential
46+
])
47+
})
48+
49+
it("should handle custom presentation types", () => {
50+
const customType = "CustomPresentation"
51+
const presentation = createPresentation({
52+
credentials: [mockCredential],
53+
holder: mockHolder,
54+
type: customType
55+
})
56+
57+
expect(presentation.type).toEqual(["VerifiablePresentation", customType])
58+
})
59+
60+
it("should handle multiple presentation types", () => {
61+
const types = ["CustomPresentation1", "CustomPresentation2"]
62+
const presentation = createPresentation({
63+
credentials: [mockCredential],
64+
holder: mockHolder,
65+
type: types
66+
})
67+
68+
expect(presentation.type).toEqual(["VerifiablePresentation", ...types])
69+
})
70+
71+
it("should use provided issuance date", () => {
72+
const issuanceDate = new Date("2024-01-01")
73+
const presentation = createPresentation({
74+
credentials: [mockCredential],
75+
holder: mockHolder,
76+
issuanceDate
77+
})
78+
79+
expect(presentation.issuanceDate).toBe(issuanceDate.toISOString())
80+
})
81+
82+
it("should use provided expiration date", () => {
83+
const expirationDate = new Date("2024-12-31")
84+
const presentation = createPresentation({
85+
credentials: [mockCredential],
86+
holder: mockHolder,
87+
expirationDate
88+
})
89+
90+
expect(presentation.expirationDate).toBe(expirationDate.toISOString())
91+
})
92+
93+
it("should include custom ID when provided", () => {
94+
const customId = "urn:uuid:12345678-1234-5678-1234-567812345678"
95+
const presentation = createPresentation({
96+
id: customId,
97+
credentials: [mockCredential],
98+
holder: mockHolder
99+
})
100+
101+
expect(presentation.id).toBe(customId)
102+
})
103+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Verifiable, W3CCredential, W3CPresentation } from "./types"
2+
import type { DidUri } from "@agentcommercekit/did"
3+
4+
export type CreatePresentationParams = {
5+
/**
6+
* The ID of the presentation.
7+
*/
8+
id?: string
9+
/**
10+
* The type of the presentation.
11+
*/
12+
type?: string | string[]
13+
/**
14+
* The holder of the presentation.
15+
*/
16+
holder: DidUri
17+
/**
18+
* The credentials to include in the presentation.
19+
*/
20+
credentials: Verifiable<W3CCredential>[]
21+
/**
22+
* The issuance date of the presentation.
23+
*/
24+
issuanceDate?: Date
25+
/**
26+
* The expiration date of the presentation.
27+
*/
28+
expirationDate?: Date
29+
}
30+
31+
export function createPresentation({
32+
credentials,
33+
holder,
34+
id,
35+
type,
36+
issuanceDate,
37+
expirationDate
38+
}: CreatePresentationParams): W3CPresentation {
39+
const credentialTypes = [type]
40+
.flat()
41+
.filter((t): t is string => !!t && t !== "VerifiablePresentation")
42+
43+
return {
44+
"@context": ["https://www.w3.org/2018/credentials/v1"],
45+
type: ["VerifiablePresentation", ...credentialTypes],
46+
id,
47+
holder,
48+
verifiableCredential: credentials,
49+
issuanceDate: issuanceDate?.toISOString(),
50+
expirationDate: expirationDate?.toISOString()
51+
}
52+
}

packages/vc/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./create-credential"
22
export * from "./is-credential"
3-
export * from "./sign-credential"
3+
export * from "./signing/sign-credential"
4+
export * from "./signing/sign-presentation"
45
export * from "./types"
56
export * from "./revocation/make-revocable"
67
export * from "./revocation/status-list-credential"

packages/vc/src/sign-credential.test.ts renamed to packages/vc/src/signing/sign-credential.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
import { createJwtSigner, verifyJwt } from "@agentcommercekit/jwt"
77
import { generateKeypair } from "@agentcommercekit/keys"
88
import { expect, test } from "vitest"
9-
import { createCredential } from "./create-credential"
9+
import { createCredential } from "../create-credential"
1010
import { signCredential } from "./sign-credential"
11-
import type { JwtCredentialPayload } from "./types"
11+
import type { JwtCredentialPayload } from "../types"
1212

1313
test("signCredential creates a valid JWT and verifiable credential", async () => {
1414
const resolver = getDidResolver()

packages/vc/src/sign-credential.ts renamed to packages/vc/src/signing/sign-credential.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
11
import { isJwtString, resolveJwtAlgorithm } from "@agentcommercekit/jwt"
22
import { createVerifiableCredentialJwt, verifyCredential } from "did-jwt-vc"
3-
import type { Verifiable, W3CCredential } from "./types"
4-
import type { Resolvable } from "@agentcommercekit/did"
5-
import type { JwtAlgorithm, JwtSigner, JwtString } from "@agentcommercekit/jwt"
6-
7-
interface SignCredentialOptions {
8-
/**
9-
* The algorithm to use for the JWT
10-
*/
11-
alg?: JwtAlgorithm
12-
/**
13-
* The DID of the credential issuer
14-
*/
15-
did: string
16-
/**
17-
* The signer to use for the JWT
18-
*/
19-
signer: JwtSigner
20-
/**
21-
* A resolver to use for parsing the signed credential
22-
*/
23-
resolver: Resolvable
24-
}
3+
import type { SignOptions } from "./types"
4+
import type { Verifiable, W3CCredential } from "../types"
5+
import type { JwtString } from "@agentcommercekit/jwt"
256

267
type SignedCredential<T extends W3CCredential> = {
278
/**
@@ -43,7 +24,7 @@ type SignedCredential<T extends W3CCredential> = {
4324
*/
4425
export async function signCredential<T extends W3CCredential>(
4526
credential: T,
46-
options: SignCredentialOptions
27+
options: SignOptions
4728
): Promise<SignedCredential<T>> {
4829
options.alg = options.alg ? resolveJwtAlgorithm(options.alg) : options.alg
4930
const jwt = await createVerifiableCredentialJwt(credential, options)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
createDidDocumentFromKeypair,
3+
createDidWebUri,
4+
getDidResolver
5+
} from "@agentcommercekit/did"
6+
import { createJwtSigner, verifyJwt } from "@agentcommercekit/jwt"
7+
import { generateKeypair } from "@agentcommercekit/keys"
8+
import { expect, test } from "vitest"
9+
import { createPresentation } from "../create-presentation"
10+
import { signPresentation } from "./sign-presentation"
11+
import type { Verifiable, W3CCredential } from "../types"
12+
13+
test("signPresentation creates a valid JWT and verifiable presentation", async () => {
14+
const resolver = getDidResolver()
15+
const holderKeypair = await generateKeypair("secp256k1")
16+
const holderDid = createDidWebUri("https://holder.example.com")
17+
18+
resolver.addToCache(
19+
holderDid,
20+
createDidDocumentFromKeypair({
21+
did: holderDid,
22+
keypair: holderKeypair
23+
})
24+
)
25+
26+
// Create a mock credential for the presentation
27+
const mockCredential: Verifiable<W3CCredential> = {
28+
"@context": ["https://www.w3.org/2018/credentials/v1"],
29+
type: ["VerifiableCredential"],
30+
issuer: { id: "did:example:issuer" },
31+
credentialSubject: { id: "did:example:subject" },
32+
issuanceDate: new Date().toISOString(),
33+
proof: {
34+
type: "Ed25519Signature2018"
35+
}
36+
}
37+
38+
// Generate an unsigned presentation
39+
const presentation = createPresentation({
40+
credentials: [mockCredential],
41+
holder: holderDid,
42+
id: "test-presentation",
43+
type: "TestPresentation"
44+
})
45+
46+
// Sign the presentation
47+
const { jwt, verifiablePresentation } = await signPresentation(presentation, {
48+
did: holderDid,
49+
signer: createJwtSigner(holderKeypair),
50+
alg: "ES256K",
51+
resolver
52+
})
53+
54+
// Verify the JWT using did-jwt verifier
55+
const result = await verifyJwt(jwt, {
56+
resolver
57+
})
58+
59+
expect(result.payload.iss).toBe(holderDid)
60+
expect(verifiablePresentation).toMatchObject(result.payload.vp)
61+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { isJwtString, resolveJwtAlgorithm } from "@agentcommercekit/jwt"
2+
import { createVerifiablePresentationJwt, verifyPresentation } from "did-jwt-vc"
3+
import type { SignOptions } from "./types"
4+
import type { Verifiable, W3CPresentation } from "../types"
5+
import type { JwtString } from "@agentcommercekit/jwt"
6+
7+
type SignedPresentation = {
8+
/**
9+
* The signed {@link Verifiable<W3CPresentation>} presentation
10+
*/
11+
verifiablePresentation: Verifiable<W3CPresentation>
12+
/**
13+
* The JWT string representation of the signed presentation
14+
*/
15+
jwt: JwtString
16+
}
17+
18+
/**
19+
* Signs a presentation with a given holder.
20+
*
21+
* @param presentation - The {@link W3CPresentation} to sign
22+
* @param options - The {@link SignCredentialOptions} to use
23+
* @returns A {@link SignedPresentation}
24+
*/
25+
export async function signPresentation(
26+
presentation: W3CPresentation,
27+
options: SignOptions
28+
): Promise<SignedPresentation> {
29+
options.alg = options.alg ? resolveJwtAlgorithm(options.alg) : options.alg
30+
const jwt = await createVerifiablePresentationJwt(presentation, options)
31+
32+
if (!isJwtString(jwt)) {
33+
throw new Error("Failed to sign presentation")
34+
}
35+
36+
const { verifiablePresentation } = await verifyPresentation(
37+
jwt,
38+
options.resolver
39+
)
40+
41+
return { jwt, verifiablePresentation }
42+
}

packages/vc/src/signing/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Resolvable } from "@agentcommercekit/did"
2+
import type { JwtAlgorithm, JwtSigner } from "@agentcommercekit/jwt"
3+
4+
export interface SignOptions {
5+
/**
6+
* The algorithm to use for the JWT
7+
*/
8+
alg?: JwtAlgorithm
9+
/**
10+
* The DID of the credential issuer
11+
*/
12+
did: string
13+
/**
14+
* The signer to use for the JWT
15+
*/
16+
signer: JwtSigner
17+
/**
18+
* A resolver to use for parsing the signed credential
19+
*/
20+
resolver: Resolvable
21+
}

packages/vc/src/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,15 @@ type W3CCredential = {
2424
termsOfUse?: any
2525
}
2626

27+
type W3CPresentation = {
28+
"@context": string[]
29+
type: string[]
30+
id?: string
31+
verifiableCredential?: Verifiable<W3CCredential>[]
32+
holder: string
33+
issuanceDate?: string
34+
expirationDate?: string
35+
}
36+
2737
export type CredentialSubject = W3CCredential["credentialSubject"]
28-
export type { JwtCredentialPayload, Verifiable, W3CCredential }
38+
export type { JwtCredentialPayload, Verifiable, W3CCredential, W3CPresentation }

0 commit comments

Comments
 (0)