@signwho/verify 0.1.0
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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/chunk-HSRXDJDV.js +38 -0
- package/dist/chunk-HSRXDJDV.js.map +1 -0
- package/dist/crypto.cjs +46 -0
- package/dist/crypto.cjs.map +1 -0
- package/dist/crypto.d.cts +19 -0
- package/dist/crypto.d.ts +19 -0
- package/dist/crypto.js +3 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.cjs +481 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +1 -0
- package/dist/sample.cjs +67 -0
- package/dist/sample.cjs.map +1 -0
- package/dist/sample.d.cts +6 -0
- package/dist/sample.d.ts +6 -0
- package/dist/sample.js +64 -0
- package/dist/sample.js.map +1 -0
- package/dist/verify-checks-BTlBVglW.d.cts +68 -0
- package/dist/verify-checks-BTlBVglW.d.ts +68 -0
- package/package.json +66 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { V as VerificationReport } from './verify-checks-BTlBVglW.cjs';
|
|
2
|
+
export { A as AssuranceTier, C as CheckResult, a as CheckStatus, E as Evidence, T as TSA_GAP_NOTE_THRESHOLD_MS, c as checkFileIntegrity, b as checkTriangleHash, d as checkTsa, e as checkWebAuthn, f as deriveTier, g as describeGap, h as extractEmbeddedEvidence, r as runAllChecks, s as selfieNote, t as timestampGapNote, v as verifyPackage } from './verify-checks-BTlBVglW.cjs';
|
|
3
|
+
export { base64UrlToBytes, bytesToBase64Url, computeTriangleHash, computeWebAuthnChallenge, hashArrayBuffer, hashData, uint8ToBase64 } from './crypto.cjs';
|
|
4
|
+
|
|
5
|
+
interface DisputeSummaryInputs {
|
|
6
|
+
report: VerificationReport;
|
|
7
|
+
generatedAt: Date;
|
|
8
|
+
pdfName?: string;
|
|
9
|
+
evidenceFileName?: string;
|
|
10
|
+
evidenceSource: 'standalone' | 'embedded' | null;
|
|
11
|
+
isSample?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface DisputeSummary {
|
|
14
|
+
text: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
}
|
|
17
|
+
declare const VERIFY_URL = "https://signwho.com/verify";
|
|
18
|
+
declare function utcStamp(value: number | string | null | undefined): string | null;
|
|
19
|
+
declare function disputeSummaryFilename(documentName: string | undefined): string;
|
|
20
|
+
declare function buildDisputeSummary(input: DisputeSummaryInputs): DisputeSummary;
|
|
21
|
+
|
|
22
|
+
interface WebAuthnProof {
|
|
23
|
+
credentialId: string;
|
|
24
|
+
publicKey: string;
|
|
25
|
+
alg: number;
|
|
26
|
+
authenticatorData: string;
|
|
27
|
+
clientDataJSON: string;
|
|
28
|
+
signature: string;
|
|
29
|
+
challenge: string;
|
|
30
|
+
challengeNonce: string;
|
|
31
|
+
}
|
|
32
|
+
interface TsaTimestamp {
|
|
33
|
+
token: string;
|
|
34
|
+
genTime: string | null;
|
|
35
|
+
serial: string | null;
|
|
36
|
+
tsa: string;
|
|
37
|
+
tsaUrl: string;
|
|
38
|
+
}
|
|
39
|
+
interface AuditEvent {
|
|
40
|
+
type: 'document_created' | 'document_viewed' | 'document_signed';
|
|
41
|
+
timestamp: number;
|
|
42
|
+
ip: string | null;
|
|
43
|
+
senderEmail: string | null;
|
|
44
|
+
signerEmail: string | null;
|
|
45
|
+
}
|
|
46
|
+
interface SignatureEvidence {
|
|
47
|
+
signerName: string;
|
|
48
|
+
documentName: string;
|
|
49
|
+
documentHash: string;
|
|
50
|
+
selfieHash?: string;
|
|
51
|
+
biometricTimestamp?: number;
|
|
52
|
+
authMethod: 'biometric' | 'passkey' | 'manual';
|
|
53
|
+
triangleHash?: string;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
faceDetected?: boolean;
|
|
56
|
+
selfieIncluded?: boolean;
|
|
57
|
+
signedPdfHash?: string;
|
|
58
|
+
webauthnProof?: WebAuthnProof;
|
|
59
|
+
tsaTimestamp?: TsaTimestamp;
|
|
60
|
+
auditEvents?: AuditEvent[];
|
|
61
|
+
senderEmail?: string;
|
|
62
|
+
completedAt?: number;
|
|
63
|
+
signedPdfAuditHistoryOmitted?: true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
declare const FREETSA_CA_PEM = "\n-----BEGIN CERTIFICATE-----\nMIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD\nVQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy\nZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw\nEAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw\nHhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl\nZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y\nZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ\nV3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql\nmQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx\n0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel\nb+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm\nq4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V\nDYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd\nEcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3\nSciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT\nFNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB\nBF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837\ngYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq\nM2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD\nAgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/\ngBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU\nU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi\nMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl\ncnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw\nMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh\nLmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB\nFidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB\nBQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG\nCCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh\ncmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH\nMAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC\nAgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt\nnxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF\nILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7\ngDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz\naWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ\nDctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4\n1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW\n99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb\n8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb\nUt5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA\nI2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=\n-----END CERTIFICATE-----\n";
|
|
67
|
+
|
|
68
|
+
export { type AuditEvent, type DisputeSummary, type DisputeSummaryInputs, FREETSA_CA_PEM, type SignatureEvidence, type TsaTimestamp, VERIFY_URL, VerificationReport, type WebAuthnProof, buildDisputeSummary, disputeSummaryFilename, utcStamp };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { V as VerificationReport } from './verify-checks-BTlBVglW.js';
|
|
2
|
+
export { A as AssuranceTier, C as CheckResult, a as CheckStatus, E as Evidence, T as TSA_GAP_NOTE_THRESHOLD_MS, c as checkFileIntegrity, b as checkTriangleHash, d as checkTsa, e as checkWebAuthn, f as deriveTier, g as describeGap, h as extractEmbeddedEvidence, r as runAllChecks, s as selfieNote, t as timestampGapNote, v as verifyPackage } from './verify-checks-BTlBVglW.js';
|
|
3
|
+
export { base64UrlToBytes, bytesToBase64Url, computeTriangleHash, computeWebAuthnChallenge, hashArrayBuffer, hashData, uint8ToBase64 } from './crypto.js';
|
|
4
|
+
|
|
5
|
+
interface DisputeSummaryInputs {
|
|
6
|
+
report: VerificationReport;
|
|
7
|
+
generatedAt: Date;
|
|
8
|
+
pdfName?: string;
|
|
9
|
+
evidenceFileName?: string;
|
|
10
|
+
evidenceSource: 'standalone' | 'embedded' | null;
|
|
11
|
+
isSample?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface DisputeSummary {
|
|
14
|
+
text: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
}
|
|
17
|
+
declare const VERIFY_URL = "https://signwho.com/verify";
|
|
18
|
+
declare function utcStamp(value: number | string | null | undefined): string | null;
|
|
19
|
+
declare function disputeSummaryFilename(documentName: string | undefined): string;
|
|
20
|
+
declare function buildDisputeSummary(input: DisputeSummaryInputs): DisputeSummary;
|
|
21
|
+
|
|
22
|
+
interface WebAuthnProof {
|
|
23
|
+
credentialId: string;
|
|
24
|
+
publicKey: string;
|
|
25
|
+
alg: number;
|
|
26
|
+
authenticatorData: string;
|
|
27
|
+
clientDataJSON: string;
|
|
28
|
+
signature: string;
|
|
29
|
+
challenge: string;
|
|
30
|
+
challengeNonce: string;
|
|
31
|
+
}
|
|
32
|
+
interface TsaTimestamp {
|
|
33
|
+
token: string;
|
|
34
|
+
genTime: string | null;
|
|
35
|
+
serial: string | null;
|
|
36
|
+
tsa: string;
|
|
37
|
+
tsaUrl: string;
|
|
38
|
+
}
|
|
39
|
+
interface AuditEvent {
|
|
40
|
+
type: 'document_created' | 'document_viewed' | 'document_signed';
|
|
41
|
+
timestamp: number;
|
|
42
|
+
ip: string | null;
|
|
43
|
+
senderEmail: string | null;
|
|
44
|
+
signerEmail: string | null;
|
|
45
|
+
}
|
|
46
|
+
interface SignatureEvidence {
|
|
47
|
+
signerName: string;
|
|
48
|
+
documentName: string;
|
|
49
|
+
documentHash: string;
|
|
50
|
+
selfieHash?: string;
|
|
51
|
+
biometricTimestamp?: number;
|
|
52
|
+
authMethod: 'biometric' | 'passkey' | 'manual';
|
|
53
|
+
triangleHash?: string;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
faceDetected?: boolean;
|
|
56
|
+
selfieIncluded?: boolean;
|
|
57
|
+
signedPdfHash?: string;
|
|
58
|
+
webauthnProof?: WebAuthnProof;
|
|
59
|
+
tsaTimestamp?: TsaTimestamp;
|
|
60
|
+
auditEvents?: AuditEvent[];
|
|
61
|
+
senderEmail?: string;
|
|
62
|
+
completedAt?: number;
|
|
63
|
+
signedPdfAuditHistoryOmitted?: true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
declare const FREETSA_CA_PEM = "\n-----BEGIN CERTIFICATE-----\nMIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD\nVQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy\nZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw\nEAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw\nHhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl\nZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y\nZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ\nV3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql\nmQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx\n0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel\nb+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm\nq4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V\nDYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd\nEcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3\nSciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT\nFNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB\nBF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837\ngYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq\nM2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD\nAgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/\ngBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU\nU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi\nMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl\ncnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw\nMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh\nLmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB\nFidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB\nBQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG\nCCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh\ncmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH\nMAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC\nAgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt\nnxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF\nILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7\ngDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz\naWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ\nDctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4\n1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW\n99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb\n8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb\nUt5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA\nI2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=\n-----END CERTIFICATE-----\n";
|
|
67
|
+
|
|
68
|
+
export { type AuditEvent, type DisputeSummary, type DisputeSummaryInputs, FREETSA_CA_PEM, type SignatureEvidence, type TsaTimestamp, VERIFY_URL, VerificationReport, type WebAuthnProof, buildDisputeSummary, disputeSummaryFilename, utcStamp };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { hashArrayBuffer, computeTriangleHash, computeWebAuthnChallenge, base64UrlToBytes } from './chunk-HSRXDJDV.js';
|
|
2
|
+
export { base64UrlToBytes, bytesToBase64Url, computeTriangleHash, computeWebAuthnChallenge, hashArrayBuffer, hashData, uint8ToBase64 } from './chunk-HSRXDJDV.js';
|
|
3
|
+
import * as pkijs from 'pkijs';
|
|
4
|
+
import { PDFDocument, PDFName, PDFDict, PDFArray, PDFRawStream } from 'pdf-lib';
|
|
5
|
+
import { inflate } from 'pako';
|
|
6
|
+
|
|
7
|
+
// src/freetsa-cacert.ts
|
|
8
|
+
var FREETSA_CA_PEM = `
|
|
9
|
+
-----BEGIN CERTIFICATE-----
|
|
10
|
+
MIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD
|
|
11
|
+
VQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy
|
|
12
|
+
ZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw
|
|
13
|
+
EAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw
|
|
14
|
+
HhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl
|
|
15
|
+
ZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y
|
|
16
|
+
ZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ
|
|
17
|
+
V3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq
|
|
18
|
+
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql
|
|
19
|
+
mQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx
|
|
20
|
+
0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel
|
|
21
|
+
b+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm
|
|
22
|
+
q4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V
|
|
23
|
+
DYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd
|
|
24
|
+
EcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3
|
|
25
|
+
SciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT
|
|
26
|
+
FNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB
|
|
27
|
+
BF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837
|
|
28
|
+
gYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq
|
|
29
|
+
M2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD
|
|
30
|
+
AgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/
|
|
31
|
+
gBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU
|
|
32
|
+
U0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi
|
|
33
|
+
MCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl
|
|
34
|
+
cnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw
|
|
35
|
+
MwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh
|
|
36
|
+
LmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB
|
|
37
|
+
FidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB
|
|
38
|
+
BQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG
|
|
39
|
+
CCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh
|
|
40
|
+
cmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH
|
|
41
|
+
MAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC
|
|
42
|
+
AgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt
|
|
43
|
+
nxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF
|
|
44
|
+
ILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7
|
|
45
|
+
gDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz
|
|
46
|
+
aWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ
|
|
47
|
+
DctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4
|
|
48
|
+
1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW
|
|
49
|
+
99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb
|
|
50
|
+
8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb
|
|
51
|
+
Ut5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA
|
|
52
|
+
I2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=
|
|
53
|
+
-----END CERTIFICATE-----
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
// src/verify-checks.ts
|
|
57
|
+
var toHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
58
|
+
var eqHex = (a, b) => !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
59
|
+
var ab = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
60
|
+
function base64ToBytes(s) {
|
|
61
|
+
const bin = atob(s.replace(/\s+/g, ""));
|
|
62
|
+
const out = new Uint8Array(bin.length);
|
|
63
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function derEcdsaToRaw(der, size) {
|
|
67
|
+
let i = 0;
|
|
68
|
+
if (der[i++] !== 48) throw new Error("bad der seq");
|
|
69
|
+
if (der[i] & 128) {
|
|
70
|
+
i += 1 + (der[i] & 127);
|
|
71
|
+
} else i++;
|
|
72
|
+
if (der[i++] !== 2) throw new Error("bad der r");
|
|
73
|
+
let rl = der[i++];
|
|
74
|
+
let r = der.slice(i, i + rl);
|
|
75
|
+
i += rl;
|
|
76
|
+
if (der[i++] !== 2) throw new Error("bad der s");
|
|
77
|
+
let sl = der[i++];
|
|
78
|
+
let s = der.slice(i, i + sl);
|
|
79
|
+
const strip = (x) => {
|
|
80
|
+
let j = 0;
|
|
81
|
+
while (j < x.length - 1 && x[j] === 0) j++;
|
|
82
|
+
return x.slice(j);
|
|
83
|
+
};
|
|
84
|
+
r = strip(r);
|
|
85
|
+
s = strip(s);
|
|
86
|
+
const out = new Uint8Array(size * 2);
|
|
87
|
+
out.set(r, size - r.length);
|
|
88
|
+
out.set(s, size * 2 - s.length);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
var curveSize = (c) => c === "P-521" ? 66 : c === "P-384" ? 48 : 32;
|
|
92
|
+
var DIGEST_OIDS = {
|
|
93
|
+
"2.16.840.1.101.3.4.2.1": "SHA-256",
|
|
94
|
+
"2.16.840.1.101.3.4.2.2": "SHA-384",
|
|
95
|
+
"2.16.840.1.101.3.4.2.3": "SHA-512",
|
|
96
|
+
"1.3.14.3.2.26": "SHA-1"
|
|
97
|
+
};
|
|
98
|
+
function ensurePkijsEngine() {
|
|
99
|
+
try {
|
|
100
|
+
const e = pkijs.getEngine();
|
|
101
|
+
if (e && e.subtle) return;
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
pkijs.setEngine("signwho", new pkijs.CryptoEngine({ name: "signwho", crypto: globalThis.crypto }));
|
|
105
|
+
}
|
|
106
|
+
async function checkFileIntegrity(pdfBytes, ev) {
|
|
107
|
+
const id = "file", label = "File matches the signed record";
|
|
108
|
+
if (!ev.signedPdfHash) return { id, label, status: "skip", detail: "This evidence.json has no signedPdfHash. Use the STANDALONE evidence.json from the email/download (the copy embedded in the PDF omits it by design)." };
|
|
109
|
+
if (!pdfBytes) return { id, label, status: "skip", detail: "Provide the signed PDF to check it against signedPdfHash." };
|
|
110
|
+
try {
|
|
111
|
+
const hex = await hashArrayBuffer(ab(pdfBytes));
|
|
112
|
+
return eqHex(hex, ev.signedPdfHash) ? { id, label, status: "pass", detail: `SHA-256 of the PDF you provided equals evidence.signedPdfHash (${hex.slice(0, 16)}\u2026).` } : { id, label, status: "fail", detail: `SHA-256 of the provided PDF (${hex.slice(0, 16)}\u2026) does NOT match evidence.signedPdfHash (${ev.signedPdfHash.slice(0, 16)}\u2026). This is not the delivered file, or it was modified.` };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { id, label, status: "fail", detail: "Could not hash the PDF: " + e.message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function checkTriangleHash(ev) {
|
|
118
|
+
const id = "triangle", label = "Triangle Authentication binding intact";
|
|
119
|
+
if (ev.selfieIncluded === false)
|
|
120
|
+
return { id, label, status: "skip", detail: "No selfie \u2014 the signer completed without a camera capture, so there is no selfie/time/document Triangle binding for this signature (by design, not a failure)." };
|
|
121
|
+
if (!ev.selfieHash || ev.biometricTimestamp == null || !ev.documentHash || !ev.triangleHash)
|
|
122
|
+
return { id, label, status: "skip", detail: "Missing fields (selfieHash / biometricTimestamp / documentHash / triangleHash)." };
|
|
123
|
+
try {
|
|
124
|
+
const recomputed = await computeTriangleHash(ev.selfieHash, ev.biometricTimestamp, ev.documentHash);
|
|
125
|
+
return eqHex(recomputed, ev.triangleHash) ? { id, label, status: "pass", detail: "SHA-256(selfieHash || biometricTimestamp || documentHash) equals evidence.triangleHash \u2014 the three artifacts are bound together." } : { id, label, status: "fail", detail: "Recomputed triangleHash does not match evidence.triangleHash \u2014 a bound component was altered." };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { id, label, status: "fail", detail: e.message };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function checkWebAuthn(ev) {
|
|
131
|
+
const id = "webauthn", label = "Signature cryptographically verified (WebAuthn)";
|
|
132
|
+
const p = ev.webauthnProof;
|
|
133
|
+
if (!p || !p.signature) return { id, label, status: "skip", detail: "No WebAuthn assertion in this signature (signer had no usable platform/roaming authenticator \u2014 recorded as manual/passkey)." };
|
|
134
|
+
if (!ev.documentHash || !p.challengeNonce || !p.clientDataJSON || !p.authenticatorData || !p.publicKey || p.alg !== -7 && p.alg !== -257)
|
|
135
|
+
return { id, label, status: "fail", detail: "Assertion is missing required fields (publicKey/alg/authenticatorData/clientDataJSON/challengeNonce)." };
|
|
136
|
+
try {
|
|
137
|
+
const expected = await computeWebAuthnChallenge(ev.documentHash, p.challengeNonce);
|
|
138
|
+
const cdj = JSON.parse(new TextDecoder().decode(base64UrlToBytes(p.clientDataJSON)));
|
|
139
|
+
if (cdj.type !== "webauthn.get") return { id, label, status: "fail", detail: `clientDataJSON.type is "${cdj.type}", expected "webauthn.get".` };
|
|
140
|
+
if (cdj.challenge !== expected) return { id, label, status: "fail", detail: "The challenge in clientDataJSON is not SHA-256(documentHash || challengeNonce) \u2014 the assertion is not bound to this document." };
|
|
141
|
+
if (p.challenge && p.challenge !== expected) return { id, label, status: "fail", detail: "Stored challenge does not match the recomputed document-bound challenge." };
|
|
142
|
+
const authData = base64UrlToBytes(p.authenticatorData);
|
|
143
|
+
const cdjBytes = base64UrlToBytes(p.clientDataJSON);
|
|
144
|
+
const cdjHash = new Uint8Array(await crypto.subtle.digest("SHA-256", ab(cdjBytes)));
|
|
145
|
+
const signed = new Uint8Array(authData.length + cdjHash.length);
|
|
146
|
+
signed.set(authData, 0);
|
|
147
|
+
signed.set(cdjHash, authData.length);
|
|
148
|
+
const spki = base64UrlToBytes(p.publicKey);
|
|
149
|
+
const sig = base64UrlToBytes(p.signature);
|
|
150
|
+
let ok;
|
|
151
|
+
if (p.alg === -7) {
|
|
152
|
+
const key = await crypto.subtle.importKey("spki", ab(spki), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
153
|
+
ok = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, ab(derEcdsaToRaw(sig, 32)), ab(signed));
|
|
154
|
+
} else {
|
|
155
|
+
const key = await crypto.subtle.importKey("spki", ab(spki), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
|
|
156
|
+
ok = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, key, ab(sig), ab(signed));
|
|
157
|
+
}
|
|
158
|
+
if (!ok) return { id, label, status: "fail", detail: "The WebAuthn assertion signature did not verify against the stored public key." };
|
|
159
|
+
const uv = (authData[32] & 4) !== 0;
|
|
160
|
+
return { id, label, status: "pass", detail: `Assertion over the document-bound challenge verifies against the public key (${p.alg === -7 ? "ES256" : "RS256"})${uv ? ", user verification (biometric/PIN) flag set" : ""}. Proves the private key holder signed THIS document.` };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { id, label, status: "fail", detail: "Verification error: " + e.message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function checkTsa(ev) {
|
|
166
|
+
const id = "tsa", label = "Timestamp valid (FreeTSA, non-qualified)";
|
|
167
|
+
const t = ev.tsaTimestamp;
|
|
168
|
+
if (!t || !t.token) return { id, label, status: "skip", detail: "No RFC-3161 timestamp (best-effort at signing; FreeTSA may have been unreachable)." };
|
|
169
|
+
const tsaTarget = ev.triangleHash || ev.documentHash;
|
|
170
|
+
const tsaTargetName = ev.triangleHash ? "triangleHash" : "documentHash";
|
|
171
|
+
if (!tsaTarget) return { id, label, status: "fail", detail: "Cannot check the timestamp without a triangleHash or documentHash to match." };
|
|
172
|
+
try {
|
|
173
|
+
ensurePkijsEngine();
|
|
174
|
+
const tokenBytes = base64ToBytes(t.token);
|
|
175
|
+
const ci = pkijs.ContentInfo.fromBER(ab(tokenBytes));
|
|
176
|
+
const sd = new pkijs.SignedData({ schema: ci.content });
|
|
177
|
+
const si = sd.signerInfos[0];
|
|
178
|
+
const digAlg = DIGEST_OIDS[si.digestAlgorithm.algorithmId] || "SHA-256";
|
|
179
|
+
const ecView = si ? sd.encapContentInfo.eContent.valueBlock.valueHexView : new Uint8Array();
|
|
180
|
+
const eContent = ab(ecView);
|
|
181
|
+
const tst = pkijs.TSTInfo.fromBER(eContent);
|
|
182
|
+
const mi = toHex(new Uint8Array(tst.messageImprint.hashedMessage.valueBlock.valueHexView));
|
|
183
|
+
if (!eqHex(mi, tsaTarget)) return { id, label, status: "fail", detail: `The timestamp covers a different hash than evidence.${tsaTargetName}.` };
|
|
184
|
+
let md = null;
|
|
185
|
+
for (const a of si.signedAttrs?.attributes || []) if (a.type === "1.2.840.113549.1.9.4") md = new Uint8Array(a.values[0].valueBlock.valueHexView);
|
|
186
|
+
const ecHash = new Uint8Array(await crypto.subtle.digest(digAlg, eContent));
|
|
187
|
+
if (!md || toHex(md) !== toHex(ecHash)) return { id, label, status: "fail", detail: "The signed messageDigest does not match the timestamp content." };
|
|
188
|
+
const sa = new Uint8Array(si.signedAttrs.encodedValue.slice(0));
|
|
189
|
+
sa[0] = 49;
|
|
190
|
+
const signerCert = sd.certificates?.find((c) => {
|
|
191
|
+
try {
|
|
192
|
+
return c.serialNumber.isEqual(si.sid.serialNumber);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}) || sd.certificates[0];
|
|
197
|
+
const isEc = si.signatureAlgorithm.algorithmId.startsWith("1.2.840.10045");
|
|
198
|
+
let sigOk;
|
|
199
|
+
if (isEc) {
|
|
200
|
+
const pub = await signerCert.getPublicKey();
|
|
201
|
+
const raw = derEcdsaToRaw(new Uint8Array(si.signature.valueBlock.valueHexView), curveSize(pub.algorithm.namedCurve));
|
|
202
|
+
sigOk = await crypto.subtle.verify({ name: "ECDSA", hash: digAlg }, pub, ab(raw), ab(sa));
|
|
203
|
+
} else {
|
|
204
|
+
const pub = await signerCert.getPublicKey({ algorithm: { algorithm: { name: "RSASSA-PKCS1-v1_5", hash: digAlg }, usages: ["verify"] } });
|
|
205
|
+
sigOk = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, pub, ab(new Uint8Array(si.signature.valueBlock.valueHexView)), ab(sa));
|
|
206
|
+
}
|
|
207
|
+
if (!sigOk) return { id, label, status: "fail", detail: "The TSA signature over the timestamp did not verify." };
|
|
208
|
+
const genTimeDate = tst.genTime instanceof Date ? tst.genTime : new Date(String(tst.genTime));
|
|
209
|
+
if (isNaN(genTimeDate.getTime())) return { id, label, status: "fail", detail: "The timestamp has no readable genTime." };
|
|
210
|
+
const nb = signerCert.notBefore.value, na = signerCert.notAfter.value;
|
|
211
|
+
if (genTimeDate < nb || genTimeDate > na) return { id, label, status: "fail", detail: `genTime (${genTimeDate.toISOString()}) is outside the TSA signing certificate's validity window (${nb.toISOString()} \u2026 ${na.toISOString()}).` };
|
|
212
|
+
const pem = FREETSA_CA_PEM.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
213
|
+
const caDer = base64ToBytes(pem);
|
|
214
|
+
const ca = pkijs.Certificate.fromBER(ab(caDer));
|
|
215
|
+
const chain = await new pkijs.CertificateChainValidationEngine({ certs: [signerCert], trustedCerts: [ca], checkDate: genTimeDate }).verify();
|
|
216
|
+
if (!chain.result) return { id, label, status: "fail", detail: "The TSA certificate does not chain to the bundled FreeTSA CA as of genTime (code " + chain.resultCode + ")." };
|
|
217
|
+
const gt = genTimeDate.toISOString();
|
|
218
|
+
return { id, label, status: "pass", facts: { genTime: genTimeDate.toISOString(), serial: toHex(new Uint8Array(tst.serialNumber.valueBlock.valueHexView)) }, detail: `FreeTSA attests this signature's ${tsaTargetName} existed at ${gt} (RFC-3161, ${digAlg}; chain validated to the bundled FreeTSA CA as of that time). Non-qualified TSA.` };
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return { id, label, status: "fail", detail: "Timestamp parse/verify error: " + e.message };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function selfieNote(ev) {
|
|
224
|
+
if (ev.selfieIncluded === false) {
|
|
225
|
+
return {
|
|
226
|
+
id: "selfie",
|
|
227
|
+
label: "No selfie \u2014 not captured",
|
|
228
|
+
status: "info",
|
|
229
|
+
detail: `This signature was completed WITHOUT a selfie (the signer couldn't or chose not to use a camera). ${ev.webauthnProof ? "It is device-verified (WebAuthn) and timestamped, but carries no photographic evidence and no identity binding." : "It carries neither a selfie nor a device signature \u2014 it is document + timestamp only, the lowest assurance tier."}`
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
id: "selfie",
|
|
234
|
+
label: "Selfie captured \u2014 not identity-verified",
|
|
235
|
+
status: "info",
|
|
236
|
+
detail: `A selfie was captured at signing (hash bound into the Triangle, verified above). ${ev.faceDetected ? "A face was detected by an in-browser heuristic. " : ""}This is NOT identity verification \u2014 SignWho does not match the face to a government ID or a real-world identity.`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
var TSA_GAP_NOTE_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
240
|
+
function describeGap(ms) {
|
|
241
|
+
const abs = Math.abs(ms);
|
|
242
|
+
const minutes = Math.round(abs / 6e4);
|
|
243
|
+
if (minutes < 120) return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
244
|
+
const hours = Math.round(abs / 36e5);
|
|
245
|
+
if (hours < 48) return `${hours} hours`;
|
|
246
|
+
const days = Math.round(abs / 864e5);
|
|
247
|
+
return `${days} days`;
|
|
248
|
+
}
|
|
249
|
+
function timestampGapNote(ev, tsaCheck) {
|
|
250
|
+
if (!tsaCheck || tsaCheck.status !== "pass" || !tsaCheck.facts?.genTime) return null;
|
|
251
|
+
if (typeof ev.timestamp !== "number" || !Number.isFinite(ev.timestamp)) return null;
|
|
252
|
+
const genMs = Date.parse(tsaCheck.facts.genTime);
|
|
253
|
+
if (Number.isNaN(genMs)) return null;
|
|
254
|
+
const gap = genMs - ev.timestamp;
|
|
255
|
+
if (Math.abs(gap) <= TSA_GAP_NOTE_THRESHOLD_MS) return null;
|
|
256
|
+
const when = gap > 0 ? "after" : "before";
|
|
257
|
+
return {
|
|
258
|
+
id: "tsa-gap",
|
|
259
|
+
label: `Timestamp obtained ${describeGap(gap)} ${when} the recorded signing time`,
|
|
260
|
+
status: "info",
|
|
261
|
+
detail: `The independent timestamp was obtained ${describeGap(gap)} ${when} the recorded signing time (genTime ${tsaCheck.facts.genTime}, evidence.timestamp ${new Date(ev.timestamp).toISOString()}). The token is still valid \u2014 it proves the hash existed at genTime \u2014 but the signer's declared time and the third-party time disagree by more than ${TSA_GAP_NOTE_THRESHOLD_MS / 6e4} minutes.`,
|
|
262
|
+
facts: { gapMs: String(gap) }
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async function runAllChecks(pdfBytes, ev) {
|
|
266
|
+
const tsa = await checkTsa(ev);
|
|
267
|
+
const gap = timestampGapNote(ev, tsa);
|
|
268
|
+
return [
|
|
269
|
+
await checkFileIntegrity(pdfBytes, ev),
|
|
270
|
+
await checkWebAuthn(ev),
|
|
271
|
+
tsa,
|
|
272
|
+
await checkTriangleHash(ev),
|
|
273
|
+
selfieNote(ev),
|
|
274
|
+
...gap ? [gap] : []
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
async function extractEmbeddedEvidence(pdfBytes) {
|
|
278
|
+
try {
|
|
279
|
+
const doc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true, updateMetadata: false });
|
|
280
|
+
const names = doc.catalog.lookupMaybe(PDFName.of("Names"), PDFDict);
|
|
281
|
+
const ef = names?.lookupMaybe(PDFName.of("EmbeddedFiles"), PDFDict);
|
|
282
|
+
const arr = ef?.lookupMaybe(PDFName.of("Names"), PDFArray);
|
|
283
|
+
if (!arr) return null;
|
|
284
|
+
for (let i = 0; i + 1 < arr.size(); i += 2) {
|
|
285
|
+
const filespec = arr.lookup(i + 1, PDFDict);
|
|
286
|
+
const efDict = filespec.lookupMaybe(PDFName.of("EF"), PDFDict);
|
|
287
|
+
const stream = efDict?.lookup(PDFName.of("F"));
|
|
288
|
+
if (!stream || !(stream instanceof PDFRawStream)) continue;
|
|
289
|
+
let bytes = stream.contents;
|
|
290
|
+
const filter = stream.dict.get(PDFName.of("Filter"));
|
|
291
|
+
if (filter && filter.toString().includes("FlateDecode")) bytes = inflate(bytes);
|
|
292
|
+
const text = new TextDecoder().decode(bytes);
|
|
293
|
+
if (text.trim().startsWith("{")) return JSON.parse(text);
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
function deriveTier(ev, checks) {
|
|
300
|
+
const noSelfie = ev.selfieIncluded === false || !ev.triangleHash && !ev.selfieHash;
|
|
301
|
+
if (!noSelfie) return "identity";
|
|
302
|
+
const webauthnPass = checks.find((c) => c.id === "webauthn")?.status === "pass";
|
|
303
|
+
return webauthnPass ? "device" : "floor";
|
|
304
|
+
}
|
|
305
|
+
async function verifyPackage(pdfBytes, ev) {
|
|
306
|
+
const checks = await runAllChecks(pdfBytes, ev);
|
|
307
|
+
const passes = checks.filter((c) => c.status === "pass").length;
|
|
308
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
309
|
+
return { evidence: ev, checks, tier: deriveTier(ev, checks), passes, fails, allGood: fails === 0 && passes > 0 };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/dispute-summary.ts
|
|
313
|
+
var VERIFY_URL = "https://signwho.com/verify";
|
|
314
|
+
var NOT_RECORDED = "not recorded in this package";
|
|
315
|
+
function utcStamp(value) {
|
|
316
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
317
|
+
const d = new Date(value);
|
|
318
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
319
|
+
return d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, " UTC");
|
|
320
|
+
}
|
|
321
|
+
function disputeSummaryFilename(documentName) {
|
|
322
|
+
const base = (documentName || "").replace(/\.pdf$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
323
|
+
return `SignWho-Dispute-Summary-${base || "document"}.txt`;
|
|
324
|
+
}
|
|
325
|
+
var TIER_LABEL = {
|
|
326
|
+
identity: "Identity-bound \u2014 selfie, device signature and independent timestamp",
|
|
327
|
+
device: "Device-verified, no selfie \u2014 device signature and independent timestamp",
|
|
328
|
+
floor: "Document + timestamp only \u2014 neither identity nor device verified"
|
|
329
|
+
};
|
|
330
|
+
var TAG = { pass: "PASS", fail: "FAIL", skip: "N/A ", info: "NOTE" };
|
|
331
|
+
function buildDisputeSummary(input) {
|
|
332
|
+
const { report, generatedAt, evidenceSource, isSample } = input;
|
|
333
|
+
const ev = report.evidence;
|
|
334
|
+
const events = ev.auditEvents || [];
|
|
335
|
+
const created = events.find((e) => e.type === "document_created");
|
|
336
|
+
const viewed = events.find((e) => e.type === "document_viewed");
|
|
337
|
+
const signed = events.find((e) => e.type === "document_signed");
|
|
338
|
+
const customerName = ev.signerName || NOT_RECORDED;
|
|
339
|
+
const customerEmail = signed?.signerEmail || viewed?.signerEmail || NOT_RECORDED;
|
|
340
|
+
const merchantEmail = ev.senderEmail || created?.senderEmail || NOT_RECORDED;
|
|
341
|
+
const signedAt = utcStamp(signed?.timestamp ?? ev.completedAt ?? ev.timestamp);
|
|
342
|
+
const tsaCheck = report.checks.find((c) => c.id === "tsa");
|
|
343
|
+
const tsaVerified = tsaCheck?.status === "pass" && !!tsaCheck.facts?.genTime;
|
|
344
|
+
const genTime = utcStamp(tsaVerified ? tsaCheck.facts.genTime : ev.tsaTimestamp?.genTime);
|
|
345
|
+
const tsaSerial = tsaVerified ? tsaCheck.facts.serial : ev.tsaTimestamp?.serial;
|
|
346
|
+
const noSelfie = ev.selfieIncluded === false || !ev.selfieHash && !ev.triangleHash;
|
|
347
|
+
const ipValue = (e) => e?.ip ? `${e.ip} (${utcStamp(e.timestamp)})` : null;
|
|
348
|
+
const L = [];
|
|
349
|
+
const section = (n, title, stripe) => {
|
|
350
|
+
L.push("", `${n}. ${title}`, ` (Stripe evidence fields: ${stripe})`);
|
|
351
|
+
};
|
|
352
|
+
const row = (label, value) => {
|
|
353
|
+
L.push(` ${label.padEnd(28)}${value || NOT_RECORDED}`);
|
|
354
|
+
};
|
|
355
|
+
L.push("SIGNWHO \u2014 PAYMENT DISPUTE SUMMARY");
|
|
356
|
+
L.push(`Generated ${utcStamp(generatedAt.getTime())} by the independent verifier at ${VERIFY_URL}.`);
|
|
357
|
+
L.push("Every check below ran in the browser from the two files alone; no SignWho server was consulted.");
|
|
358
|
+
if (isSample) L.push("THIS IS SIGNWHO'S BUNDLED SAMPLE PACKAGE \u2014 a demonstration, not a real agreement.");
|
|
359
|
+
if (!report.allGood) {
|
|
360
|
+
L.push("", `WARNING: ${report.fails} check(s) FAILED \u2014 this package does not verify. Do not rely on it as-is.`);
|
|
361
|
+
}
|
|
362
|
+
section(1, "CUSTOMER", "customer_name, customer_email_address");
|
|
363
|
+
row("Name (as signed):", customerName);
|
|
364
|
+
row("E-mail:", customerEmail);
|
|
365
|
+
row("Assurance level:", TIER_LABEL[report.tier]);
|
|
366
|
+
section(2, "CUSTOMER IP ADDRESSES", "customer_purchase_ip");
|
|
367
|
+
row("Viewed the document from:", ipValue(viewed));
|
|
368
|
+
row("Signed the document from:", ipValue(signed));
|
|
369
|
+
section(3, "TIMESTAMPS (UTC)", "service_date, customer_communication");
|
|
370
|
+
row("Document sent:", utcStamp(created?.timestamp));
|
|
371
|
+
row("Document viewed:", utcStamp(viewed?.timestamp));
|
|
372
|
+
row("Document signed:", signedAt);
|
|
373
|
+
row("Independent timestamp:", genTime ? `${genTime} \u2014 ${ev.tsaTimestamp?.tsa || "RFC 3161 TSA"}${tsaSerial ? `, token serial ${tsaSerial}` : ""} ${tsaVerified ? "(RFC 3161, third party)" : "(declared in the evidence, NOT verified by the timestamp check)"}` : null);
|
|
374
|
+
const gapNote = report.checks.find((c) => c.id === "tsa-gap");
|
|
375
|
+
if (gapNote) L.push(` NOTE: ${gapNote.detail}`);
|
|
376
|
+
section(4, "DOCUMENT", "customer_signature, service_documentation");
|
|
377
|
+
row("Document name:", ev.documentName);
|
|
378
|
+
row("Sent by (merchant):", merchantEmail);
|
|
379
|
+
row("SHA-256, signed PDF:", ev.signedPdfHash || (evidenceSource === "embedded" ? "not present \u2014 the evidence was read from inside the PDF; the standalone evidence.json carries the file hash" : null));
|
|
380
|
+
row("SHA-256, original document:", ev.documentHash);
|
|
381
|
+
row("Signature method:", ev.authMethod === "biometric" ? "device biometric (Face ID / fingerprint / Windows Hello) \u2014 WebAuthn assertion" : ev.authMethod === "passkey" ? "device passkey \u2014 WebAuthn assertion" : ev.authMethod === "manual" ? "manual (no device signature)" : ev.webauthnProof ? "WebAuthn assertion" : null);
|
|
382
|
+
row("Customer's face:", noSelfie ? "no selfie was captured for this signature" : `a selfie was captured at signing (SHA-256 ${ev.selfieHash || NOT_RECORDED}); the image is printed on the certificate page of the signed PDF`);
|
|
383
|
+
section(5, `VERIFICATION RESULTS \u2014 ${report.passes} of ${report.checks.filter((c) => c.status !== "info").length} checks passed`, "uncategorized_text");
|
|
384
|
+
for (const c of report.checks) {
|
|
385
|
+
L.push(` [${TAG[c.status]}] ${c.label}`);
|
|
386
|
+
L.push(` ${c.detail}`);
|
|
387
|
+
}
|
|
388
|
+
L.push("", "6. HOW TO RE-VERIFY (for the card network or the customer's bank)");
|
|
389
|
+
L.push(` Open the signed PDF and the evidence.json at ${VERIFY_URL} \u2014 the checks run in the browser, nothing is uploaded.`);
|
|
390
|
+
L.push(" Attach both files to the dispute: the signed PDF as the customer's signature, the evidence.json as the service documentation.");
|
|
391
|
+
if (input.pdfName || input.evidenceFileName) {
|
|
392
|
+
L.push(` Files verified for this summary: ${[input.pdfName, input.evidenceFileName].filter(Boolean).join(", ")}`);
|
|
393
|
+
}
|
|
394
|
+
L.push(" The algorithms are public standards (SHA-256, WebAuthn/FIDO2, RFC 3161); anyone can re-run them with open tools.");
|
|
395
|
+
L.push("");
|
|
396
|
+
return { text: L.join("\n"), filename: disputeSummaryFilename(ev.documentName) };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export { FREETSA_CA_PEM, TSA_GAP_NOTE_THRESHOLD_MS, VERIFY_URL, buildDisputeSummary, checkFileIntegrity, checkTriangleHash, checkTsa, checkWebAuthn, deriveTier, describeGap, disputeSummaryFilename, extractEmbeddedEvidence, runAllChecks, selfieNote, timestampGapNote, utcStamp, verifyPackage };
|
|
400
|
+
//# sourceMappingURL=index.js.map
|
|
401
|
+
//# sourceMappingURL=index.js.map
|