vet-sdk-core-ts 0.4.25 → 0.4.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/health-card-issuance.d.ts +31 -0
- package/dist/health-card-issuance.js +57 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/secure-clinic-channel.d.ts +50 -0
- package/dist/secure-clinic-channel.js +173 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -32,6 +32,22 @@ The result contains printable vaccination data plus both
|
|
|
32
32
|
the validity interval explicitly; FHIR `Immunization.expirationDate` remains
|
|
33
33
|
the vaccine-batch expiry.
|
|
34
34
|
|
|
35
|
+
`pqcProofLookupReference` is separate: it hashes the exact compact SHC JWS so
|
|
36
|
+
`publishVeterinaryHealthCardPqcProofs(...)` and
|
|
37
|
+
`resolveVeterinaryHealthCardPqcProofs(...)` can use a product ledger/resolver
|
|
38
|
+
to retrieve the unchanged detached `pqc:/` proofs from a single printed SHC
|
|
39
|
+
QR when online. It does not replace the offline companion QR labels.
|
|
40
|
+
|
|
41
|
+
## Private clinic documents and index projection
|
|
42
|
+
|
|
43
|
+
`sealVeterinarySecureClinicMessage(...)` signs the complete FHIR Bundle
|
|
44
|
+
document with ML-DSA-44 and encrypts it to the clinic/controller endpoint with
|
|
45
|
+
ML-KEM-768 plus AES-256-GCM. Its separate index projection contains only each
|
|
46
|
+
source `fullUrl` and the server-policy allowlisted `resource.meta.tag[]`
|
|
47
|
+
codings. Every coding system must be the concrete
|
|
48
|
+
`<ResourceType>.<search-parameter-or-custom>` key; repeated values become one
|
|
49
|
+
CSV flat claim. The GW never receives the encrypted message or private Bundle.
|
|
50
|
+
|
|
35
51
|
## USDC payment quotes
|
|
36
52
|
|
|
37
53
|
`vet-sdk-core-ts/payment` separates the payment provider, asset and EVM network
|
|
@@ -13,8 +13,26 @@ export type IssuedVeterinaryHealthCardCredential = Readonly<{
|
|
|
13
13
|
pqcQr: readonly (readonly string[])[];
|
|
14
14
|
payloadBytes: Uint8Array;
|
|
15
15
|
payloadReferences: SmartHealthCardPayloadReferences;
|
|
16
|
+
/** Optional online lookup; separate from payload hashes and the `pqc:/` format. */
|
|
17
|
+
pqcProofLookupReference: string;
|
|
16
18
|
printData: InternationalHealthCardPrintData;
|
|
17
19
|
}>;
|
|
20
|
+
export type VeterinaryHealthCardPqcProofRecord = Readonly<{
|
|
21
|
+
lookupReference: string;
|
|
22
|
+
pqcCompanionProofs: readonly string[];
|
|
23
|
+
}>;
|
|
24
|
+
export type VeterinaryHealthCardPqcProofLedger<Receipt = unknown> = Readonly<{
|
|
25
|
+
publish(record: VeterinaryHealthCardPqcProofRecord): Promise<Receipt>;
|
|
26
|
+
resolve(lookupReference: string): Promise<VeterinaryHealthCardPqcProofRecord | undefined>;
|
|
27
|
+
}>;
|
|
28
|
+
/** Browser/server adapter for the Vet product BFF; ledger custody remains behind that boundary. */
|
|
29
|
+
export declare function createVeterinaryHealthCardPqcProofHttpLedger(input: Readonly<{
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
bearerToken?: () => Promise<string | undefined>;
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}>): VeterinaryHealthCardPqcProofLedger<Readonly<{
|
|
34
|
+
transactionId: string;
|
|
35
|
+
}>>;
|
|
18
36
|
/**
|
|
19
37
|
* Issues transport values only after the caller supplies GW's authoritative
|
|
20
38
|
* readback Bundle. The tenant issuer owns the standard ES256 signature. Every
|
|
@@ -35,6 +53,19 @@ export declare function issueVeterinaryHealthCardCredential(input: Readonly<{
|
|
|
35
53
|
}>;
|
|
36
54
|
pqcQr?: PostQuantumCompanionProofQrOptions;
|
|
37
55
|
}>): Promise<IssuedVeterinaryHealthCardCredential>;
|
|
56
|
+
/** Publishes only detached proofs under the exact compact-SHC lookup key. */
|
|
57
|
+
export declare function publishVeterinaryHealthCardPqcProofs<Receipt>(input: Readonly<{
|
|
58
|
+
issued: IssuedVeterinaryHealthCardCredential;
|
|
59
|
+
ledger: VeterinaryHealthCardPqcProofLedger<Receipt>;
|
|
60
|
+
}>): Promise<Readonly<{
|
|
61
|
+
lookupReference: string;
|
|
62
|
+
receipt: Receipt;
|
|
63
|
+
}>>;
|
|
64
|
+
/** Resolves companion proofs online from one scanned SHC QR set. */
|
|
65
|
+
export declare function resolveVeterinaryHealthCardPqcProofs(input: Readonly<{
|
|
66
|
+
shcQr: readonly string[];
|
|
67
|
+
ledger: Pick<VeterinaryHealthCardPqcProofLedger, 'resolve'>;
|
|
68
|
+
}>): Promise<readonly string[]>;
|
|
38
69
|
/** Verifies the standard credential and all scanned companion proofs offline through caller-owned trust resolvers. */
|
|
39
70
|
export declare function verifyVeterinaryHealthCardCredential(input: Readonly<{
|
|
40
71
|
shcQr: readonly string[];
|
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
import { assembleSmartHealthCardJws, buildSmartHealthCardPayloadReferences, decodePostQuantumCompanionProofQr, decodeSmartHealthCardPayloadBytes, decodeSmartHealthCardQr, encodePostQuantumCompanionProofUri, encodePostQuantumCompanionProofQr, encodeSmartHealthCardQr, prepareSmartHealthCard, } from 'vet-data-utils-ts/shc';
|
|
1
|
+
import { assembleSmartHealthCardJws, buildSmartHealthCardJwsProofReference, buildSmartHealthCardPayloadReferences, decodePostQuantumCompanionProofQr, decodePostQuantumCompanionProofUri, decodeSmartHealthCardPayloadBytes, decodeSmartHealthCardQr, encodePostQuantumCompanionProofUri, encodePostQuantumCompanionProofQr, encodeSmartHealthCardQr, prepareSmartHealthCard, } from 'vet-data-utils-ts/shc';
|
|
2
2
|
import { buildInternationalHealthCardPrintData, } from 'vet-data-utils-ts/international-health-card';
|
|
3
3
|
import { projectClinicalBundleForReading, } from 'vet-data-utils-ts/clinical-bundle-reader';
|
|
4
|
+
/** Browser/server adapter for the Vet product BFF; ledger custody remains behind that boundary. */
|
|
5
|
+
export function createVeterinaryHealthCardPqcProofHttpLedger(input) {
|
|
6
|
+
const baseUrl = input.baseUrl.replace(/\/$/, '');
|
|
7
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
8
|
+
const headers = async () => {
|
|
9
|
+
const token = await input.bearerToken?.();
|
|
10
|
+
return {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
async publish(record) {
|
|
17
|
+
const response = await fetchImpl(baseUrl, {
|
|
18
|
+
method: 'POST', headers: await headers(), body: JSON.stringify(record),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
throw new Error(`veterinary_health_card_pqc_publish_failed:${response.status}`);
|
|
22
|
+
return await response.json();
|
|
23
|
+
},
|
|
24
|
+
async resolve(lookupReference) {
|
|
25
|
+
const response = await fetchImpl(`${baseUrl}/${encodeURIComponent(lookupReference)}`, {
|
|
26
|
+
method: 'GET', headers: await headers(),
|
|
27
|
+
});
|
|
28
|
+
if (response.status === 404)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!response.ok)
|
|
31
|
+
throw new Error(`veterinary_health_card_pqc_resolve_failed:${response.status}`);
|
|
32
|
+
return await response.json();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
4
36
|
/**
|
|
5
37
|
* Issues transport values only after the caller supplies GW's authoritative
|
|
6
38
|
* readback Bundle. The tenant issuer owns the standard ES256 signature. Every
|
|
@@ -34,6 +66,7 @@ export async function issueVeterinaryHealthCardCredential(input) {
|
|
|
34
66
|
pqcQr: detached.map(proof => encodePostQuantumCompanionProofQr(proof, input.pqcQr)),
|
|
35
67
|
payloadBytes: prepared.payloadBytes,
|
|
36
68
|
payloadReferences,
|
|
69
|
+
pqcProofLookupReference: buildSmartHealthCardJwsProofReference(compactJws),
|
|
37
70
|
printData: buildInternationalHealthCardPrintData({
|
|
38
71
|
authoritativeBundle: input.authoritativeBundle,
|
|
39
72
|
validFrom: input.print.validFrom,
|
|
@@ -43,6 +76,29 @@ export async function issueVeterinaryHealthCardCredential(input) {
|
|
|
43
76
|
}),
|
|
44
77
|
};
|
|
45
78
|
}
|
|
79
|
+
/** Publishes only detached proofs under the exact compact-SHC lookup key. */
|
|
80
|
+
export async function publishVeterinaryHealthCardPqcProofs(input) {
|
|
81
|
+
input.issued.pqcCompanionProofs.forEach(decodePostQuantumCompanionProofUri);
|
|
82
|
+
const record = {
|
|
83
|
+
lookupReference: input.issued.pqcProofLookupReference,
|
|
84
|
+
pqcCompanionProofs: [...input.issued.pqcCompanionProofs],
|
|
85
|
+
};
|
|
86
|
+
const receipt = await input.ledger.publish(record);
|
|
87
|
+
return { lookupReference: record.lookupReference, receipt };
|
|
88
|
+
}
|
|
89
|
+
/** Resolves companion proofs online from one scanned SHC QR set. */
|
|
90
|
+
export async function resolveVeterinaryHealthCardPqcProofs(input) {
|
|
91
|
+
const compactJws = decodeSmartHealthCardQr(input.shcQr);
|
|
92
|
+
const lookupReference = buildSmartHealthCardJwsProofReference(compactJws);
|
|
93
|
+
const record = await input.ledger.resolve(lookupReference);
|
|
94
|
+
if (!record)
|
|
95
|
+
throw new TypeError('veterinary_health_card_pqc_proof_not_found');
|
|
96
|
+
if (record.lookupReference !== lookupReference) {
|
|
97
|
+
throw new TypeError('veterinary_health_card_pqc_lookup_mismatch');
|
|
98
|
+
}
|
|
99
|
+
record.pqcCompanionProofs.forEach(decodePostQuantumCompanionProofUri);
|
|
100
|
+
return [...record.pqcCompanionProofs];
|
|
101
|
+
}
|
|
46
102
|
/** Verifies the standard credential and all scanned companion proofs offline through caller-owned trust resolvers. */
|
|
47
103
|
export async function verifyVeterinaryHealthCardCredential(input) {
|
|
48
104
|
const compactJws = decodeSmartHealthCardQr(input.shcQr);
|
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export * from "./reusable-bff.js";
|
|
|
7
7
|
export * from "./research-study.js";
|
|
8
8
|
export * from "./payment.js";
|
|
9
9
|
export * from "./health-card-issuance.js";
|
|
10
|
+
export * from "./secure-clinic-channel.js";
|
|
10
11
|
export * from "./scheduling.js";
|
|
11
12
|
export * from "vet-data-utils-ts";
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,6 @@ export * from "./reusable-bff.js";
|
|
|
7
7
|
export * from "./research-study.js";
|
|
8
8
|
export * from "./payment.js";
|
|
9
9
|
export * from "./health-card-issuance.js";
|
|
10
|
+
export * from "./secure-clinic-channel.js";
|
|
10
11
|
export * from "./scheduling.js";
|
|
11
12
|
export * from "vet-data-utils-ts";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type JsonObject = Record<string, unknown>;
|
|
2
|
+
export interface VeterinarySecureEndpointPublicKeys {
|
|
3
|
+
endpointId: string;
|
|
4
|
+
signingKeyId: string;
|
|
5
|
+
encryptionKeyId: string;
|
|
6
|
+
signingPublicKey: Uint8Array;
|
|
7
|
+
encryptionPublicKey: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
export interface VeterinarySecureEndpointKeys {
|
|
10
|
+
publicKeys: VeterinarySecureEndpointPublicKeys;
|
|
11
|
+
signingSecretKey: Uint8Array;
|
|
12
|
+
encryptionSecretKey: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
export interface VeterinaryEncryptedEnvelope {
|
|
15
|
+
mediaType: 'application/didcomm-encrypted+json';
|
|
16
|
+
compactJwe: string;
|
|
17
|
+
}
|
|
18
|
+
export interface VeterinaryIndexProjection {
|
|
19
|
+
entries: Array<{
|
|
20
|
+
fullUrl: string;
|
|
21
|
+
claims: Record<string, string>;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface VeterinaryIndexProjectionPolicy {
|
|
25
|
+
allowedResourceTypes: ReadonlySet<string>;
|
|
26
|
+
allowedTagSystems: ReadonlySet<string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function createVeterinarySecureEndpointKeys(endpointId: string): Promise<VeterinarySecureEndpointKeys>;
|
|
29
|
+
export declare function sealVeterinarySecureClinicMessage(input: Readonly<{
|
|
30
|
+
sender: VeterinarySecureEndpointKeys;
|
|
31
|
+
recipient: VeterinarySecureEndpointPublicKeys;
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
messageId: string;
|
|
35
|
+
threadId: string;
|
|
36
|
+
bundle: JsonObject;
|
|
37
|
+
indexPolicy: VeterinaryIndexProjectionPolicy;
|
|
38
|
+
}>): Promise<Readonly<{
|
|
39
|
+
envelope: VeterinaryEncryptedEnvelope;
|
|
40
|
+
indexProjection: VeterinaryIndexProjection;
|
|
41
|
+
}>>;
|
|
42
|
+
export declare function openVeterinarySecureClinicMessage(input: Readonly<{
|
|
43
|
+
recipient: VeterinarySecureEndpointKeys;
|
|
44
|
+
senderSigningPublicKey: Uint8Array;
|
|
45
|
+
envelope: VeterinaryEncryptedEnvelope;
|
|
46
|
+
}>): Promise<Readonly<{
|
|
47
|
+
bundle: unknown;
|
|
48
|
+
signatureVerified: true;
|
|
49
|
+
}>>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
2
|
+
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
const decoder = new TextDecoder();
|
|
5
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
function base64Url(value) {
|
|
7
|
+
const bytes = typeof value === 'string' ? encoder.encode(value) : value;
|
|
8
|
+
let binary = '';
|
|
9
|
+
for (const byte of bytes)
|
|
10
|
+
binary += String.fromCharCode(byte);
|
|
11
|
+
return btoa(binary).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
12
|
+
}
|
|
13
|
+
function fromBase64Url(value) {
|
|
14
|
+
const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
|
|
15
|
+
const binary = atob(padded);
|
|
16
|
+
return Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
17
|
+
}
|
|
18
|
+
function canonicalize(value) {
|
|
19
|
+
if (value === null || typeof value !== 'object')
|
|
20
|
+
return JSON.stringify(value);
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return `[${value.map(canonicalize).join(',')}]`;
|
|
23
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
|
|
24
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalize(child)}`).join(',')}}`;
|
|
25
|
+
}
|
|
26
|
+
async function keyId(prefix, publicKey) {
|
|
27
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', publicKey));
|
|
28
|
+
return `${prefix}:${base64Url(digest)}`;
|
|
29
|
+
}
|
|
30
|
+
export async function createVeterinarySecureEndpointKeys(endpointId) {
|
|
31
|
+
const signing = ml_dsa44.keygen();
|
|
32
|
+
const encryption = ml_kem768.keygen();
|
|
33
|
+
return {
|
|
34
|
+
publicKeys: {
|
|
35
|
+
endpointId,
|
|
36
|
+
signingKeyId: await keyId('urn:vetchain:key:ml-dsa-44', signing.publicKey),
|
|
37
|
+
encryptionKeyId: await keyId('urn:vetchain:key:ml-kem-768', encryption.publicKey),
|
|
38
|
+
signingPublicKey: signing.publicKey,
|
|
39
|
+
encryptionPublicKey: encryption.publicKey,
|
|
40
|
+
},
|
|
41
|
+
signingSecretKey: signing.secretKey,
|
|
42
|
+
encryptionSecretKey: encryption.secretKey,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function encryptAesGcm(keyBytes, plaintext, aad) {
|
|
46
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
47
|
+
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['encrypt']);
|
|
48
|
+
const combined = new Uint8Array(await crypto.subtle.encrypt({
|
|
49
|
+
name: 'AES-GCM', iv: iv, ...(aad ? { additionalData: aad } : {}), tagLength: 128,
|
|
50
|
+
}, key, plaintext));
|
|
51
|
+
return { iv, ciphertext: combined.slice(0, -16), tag: combined.slice(-16) };
|
|
52
|
+
}
|
|
53
|
+
async function decryptAesGcm(keyBytes, iv, ciphertext, tag, aad) {
|
|
54
|
+
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt']);
|
|
55
|
+
const combined = new Uint8Array(ciphertext.length + tag.length);
|
|
56
|
+
combined.set(ciphertext);
|
|
57
|
+
combined.set(tag, ciphertext.length);
|
|
58
|
+
return new Uint8Array(await crypto.subtle.decrypt({
|
|
59
|
+
name: 'AES-GCM', iv: iv, ...(aad ? { additionalData: aad } : {}), tagLength: 128,
|
|
60
|
+
}, key, combined));
|
|
61
|
+
}
|
|
62
|
+
async function deriveWrapKey(sharedSecret, encryptionKeyId) {
|
|
63
|
+
const source = await crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false, ['deriveBits']);
|
|
64
|
+
return new Uint8Array(await crypto.subtle.deriveBits({
|
|
65
|
+
name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(0),
|
|
66
|
+
info: encoder.encode(`VetChain clinic CEK wrap v1\0${encryptionKeyId}`),
|
|
67
|
+
}, source, 256));
|
|
68
|
+
}
|
|
69
|
+
function extractIndexProjection(bundle, policy) {
|
|
70
|
+
var _a;
|
|
71
|
+
if (bundle.resourceType !== 'Bundle' || bundle.type !== 'document' || !Array.isArray(bundle.entry)) {
|
|
72
|
+
throw new Error('The private payload must be a FHIR Bundle document');
|
|
73
|
+
}
|
|
74
|
+
if (bundle.entry[0]?.resource?.resourceType !== 'Composition') {
|
|
75
|
+
throw new Error('A FHIR Bundle document must have Composition as its first entry');
|
|
76
|
+
}
|
|
77
|
+
const entries = [];
|
|
78
|
+
for (const rawEntry of bundle.entry) {
|
|
79
|
+
const entry = rawEntry;
|
|
80
|
+
const resource = entry.resource;
|
|
81
|
+
if (!resource || typeof resource.resourceType !== 'string' || typeof resource.id !== 'string')
|
|
82
|
+
continue;
|
|
83
|
+
if (typeof entry.fullUrl !== 'string')
|
|
84
|
+
throw new Error('Each indexed resource requires an absolute HTTPS fullUrl');
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = new URL(entry.fullUrl);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error('Each indexed resource requires an absolute HTTPS fullUrl');
|
|
91
|
+
}
|
|
92
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
93
|
+
if (parsed.protocol !== 'https:' || segments.at(-2) !== resource.resourceType
|
|
94
|
+
|| segments.at(-1) !== resource.id || !UUID_PATTERN.test(resource.id)) {
|
|
95
|
+
throw new Error('The fullUrl must end in /<ResourceType>/<UUID> matching the resource');
|
|
96
|
+
}
|
|
97
|
+
if (resource.resourceType === 'Binary' && resource.contentType === 'application/pdf') {
|
|
98
|
+
throw new Error('PDF content must be an Attachment inside DocumentReference');
|
|
99
|
+
}
|
|
100
|
+
if (!policy.allowedResourceTypes.has(resource.resourceType))
|
|
101
|
+
continue;
|
|
102
|
+
const tags = resource.meta?.tag;
|
|
103
|
+
if (!Array.isArray(tags))
|
|
104
|
+
continue;
|
|
105
|
+
const grouped = {};
|
|
106
|
+
for (const rawTag of tags) {
|
|
107
|
+
const tag = rawTag;
|
|
108
|
+
if (typeof tag.system !== 'string' || typeof tag.code !== 'string' || !policy.allowedTagSystems.has(tag.system))
|
|
109
|
+
continue;
|
|
110
|
+
if (!tag.system.startsWith(`${resource.resourceType}.`)) {
|
|
111
|
+
throw new Error(`Index claim ${tag.system} must start with ${resource.resourceType}.`);
|
|
112
|
+
}
|
|
113
|
+
;
|
|
114
|
+
(grouped[_a = tag.system] ?? (grouped[_a] = [])).push(tag.code);
|
|
115
|
+
}
|
|
116
|
+
const claims = Object.fromEntries(Object.entries(grouped).map(([key, values]) => [key, values.join(',')]));
|
|
117
|
+
if (Object.keys(claims).length)
|
|
118
|
+
entries.push({ fullUrl: entry.fullUrl, claims });
|
|
119
|
+
}
|
|
120
|
+
return { entries };
|
|
121
|
+
}
|
|
122
|
+
function sign(sender, plaintext) {
|
|
123
|
+
const header = base64Url(canonicalize({ alg: 'ML-DSA-44', kid: sender.publicKeys.signingKeyId, typ: 'application/didcomm-signed+json' }));
|
|
124
|
+
const payload = base64Url(canonicalize(plaintext));
|
|
125
|
+
return `${header}.${payload}.${base64Url(ml_dsa44.sign(encoder.encode(`${header}.${payload}`), sender.signingSecretKey))}`;
|
|
126
|
+
}
|
|
127
|
+
export async function sealVeterinarySecureClinicMessage(input) {
|
|
128
|
+
const indexProjection = extractIndexProjection(input.bundle, input.indexPolicy);
|
|
129
|
+
const signed = sign(input.sender, {
|
|
130
|
+
id: input.messageId, thid: input.threadId,
|
|
131
|
+
type: 'https://vetchain.app/protocols/private-fhir-document/1.0/ips',
|
|
132
|
+
from: input.from, to: [input.to], created_time: Math.floor(Date.now() / 1000),
|
|
133
|
+
body: { mediaType: 'application/fhir+json', bundle: input.bundle },
|
|
134
|
+
});
|
|
135
|
+
const cek = crypto.getRandomValues(new Uint8Array(32));
|
|
136
|
+
const header = base64Url(canonicalize({
|
|
137
|
+
alg: 'ML-KEM-768+A256GCMKW', enc: 'A256GCM', kid: input.recipient.encryptionKeyId,
|
|
138
|
+
typ: 'application/didcomm-encrypted+json', cty: 'application/didcomm-signed+json',
|
|
139
|
+
}));
|
|
140
|
+
const encrypted = await encryptAesGcm(cek, encoder.encode(signed), encoder.encode(header));
|
|
141
|
+
const encapsulated = ml_kem768.encapsulate(input.recipient.encryptionPublicKey);
|
|
142
|
+
const wrapped = await encryptAesGcm(await deriveWrapKey(encapsulated.sharedSecret, input.recipient.encryptionKeyId), cek);
|
|
143
|
+
const encryptedKey = base64Url(canonicalize({
|
|
144
|
+
kem: base64Url(encapsulated.cipherText), iv: base64Url(wrapped.iv),
|
|
145
|
+
ciphertext: base64Url(wrapped.ciphertext), tag: base64Url(wrapped.tag),
|
|
146
|
+
}));
|
|
147
|
+
return { envelope: { mediaType: 'application/didcomm-encrypted+json', compactJwe: [
|
|
148
|
+
header, encryptedKey, base64Url(encrypted.iv), base64Url(encrypted.ciphertext), base64Url(encrypted.tag),
|
|
149
|
+
].join('.') }, indexProjection };
|
|
150
|
+
}
|
|
151
|
+
export async function openVeterinarySecureClinicMessage(input) {
|
|
152
|
+
const parts = input.envelope.compactJwe.split('.');
|
|
153
|
+
if (parts.length !== 5)
|
|
154
|
+
throw new Error('Invalid encrypted DIDComm envelope');
|
|
155
|
+
const [headerPart, encryptedKeyPart, iv, ciphertext, tag] = parts;
|
|
156
|
+
const header = JSON.parse(decoder.decode(fromBase64Url(headerPart)));
|
|
157
|
+
if (header.kid !== input.recipient.publicKeys.encryptionKeyId)
|
|
158
|
+
throw new Error('Envelope recipient key does not match');
|
|
159
|
+
const encryptedKey = JSON.parse(decoder.decode(fromBase64Url(encryptedKeyPart)));
|
|
160
|
+
const shared = ml_kem768.decapsulate(fromBase64Url(encryptedKey.kem), input.recipient.encryptionSecretKey);
|
|
161
|
+
const cek = await decryptAesGcm(await deriveWrapKey(shared, input.recipient.publicKeys.encryptionKeyId), fromBase64Url(encryptedKey.iv), fromBase64Url(encryptedKey.ciphertext), fromBase64Url(encryptedKey.tag));
|
|
162
|
+
const signed = decoder.decode(await decryptAesGcm(cek, fromBase64Url(iv), fromBase64Url(ciphertext), fromBase64Url(tag), encoder.encode(headerPart)));
|
|
163
|
+
const [signedHeader, payload, signature, extra] = signed.split('.');
|
|
164
|
+
if (extra !== undefined || !signature)
|
|
165
|
+
throw new Error('Invalid signed DIDComm payload');
|
|
166
|
+
const protectedHeader = JSON.parse(decoder.decode(fromBase64Url(signedHeader)));
|
|
167
|
+
if (protectedHeader.alg !== 'ML-DSA-44' || protectedHeader.typ !== 'application/didcomm-signed+json'
|
|
168
|
+
|| !ml_dsa44.verify(fromBase64Url(signature), encoder.encode(`${signedHeader}.${payload}`), input.senderSigningPublicKey)) {
|
|
169
|
+
throw new Error('Invalid ML-DSA signature');
|
|
170
|
+
}
|
|
171
|
+
const plaintext = JSON.parse(decoder.decode(fromBase64Url(payload)));
|
|
172
|
+
return { bundle: (plaintext.body.bundle), signatureVerified: true };
|
|
173
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-sdk-core-ts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"description": "Browser-safe VetChain core contracts and governed animal species identifiers",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
"types": "./dist/health-card-issuance.d.ts",
|
|
49
49
|
"default": "./dist/health-card-issuance.js"
|
|
50
50
|
},
|
|
51
|
+
"./secure-clinic-channel": {
|
|
52
|
+
"types": "./dist/secure-clinic-channel.d.ts",
|
|
53
|
+
"default": "./dist/secure-clinic-channel.js"
|
|
54
|
+
},
|
|
51
55
|
"./scheduling": {
|
|
52
56
|
"types": "./dist/scheduling.d.ts",
|
|
53
57
|
"default": "./dist/scheduling.js"
|
|
@@ -75,7 +79,8 @@
|
|
|
75
79
|
},
|
|
76
80
|
"dependencies": {
|
|
77
81
|
"@noble/hashes": "^2.2.0",
|
|
82
|
+
"@noble/post-quantum": "0.5.4",
|
|
78
83
|
"gdc-common-utils-ts": "2.9.10",
|
|
79
|
-
"vet-data-utils-ts": "0.5.
|
|
84
|
+
"vet-data-utils-ts": "0.5.17"
|
|
80
85
|
}
|
|
81
86
|
}
|