@sd-jwt/core 0.19.1-next.0 → 0.19.1-next.2
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 +65 -0
- package/dist/index.d.mts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +155 -0
- package/dist/index.mjs +155 -0
- package/package.json +7 -7
- package/src/index.ts +199 -0
- package/src/test/index.spec.ts +152 -0
package/README.md
CHANGED
|
@@ -41,3 +41,68 @@ If you want to use the pure sd-jwt class or implement your own sd-jwt credential
|
|
|
41
41
|
- @sd-jwt/present
|
|
42
42
|
- @sd-jwt/types
|
|
43
43
|
- @sd-jwt/utils
|
|
44
|
+
|
|
45
|
+
### Verification
|
|
46
|
+
|
|
47
|
+
The library provides two verification approaches:
|
|
48
|
+
|
|
49
|
+
#### Standard Verification (Fail-Fast)
|
|
50
|
+
|
|
51
|
+
The `verify()` method throws an error immediately when the first validation failure is encountered:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
try {
|
|
55
|
+
const result = await sdjwt.verify(credential);
|
|
56
|
+
console.log('Verified payload:', result.payload);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Verification failed:', error.message);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Safe Verification (Collect All Errors)
|
|
63
|
+
|
|
64
|
+
The `safeVerify()` method collects all validation errors instead of failing on the first one. This is useful when you want to show users all issues with a credential at once:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import type { SafeVerifyResult, VerificationError } from '@sd-jwt/types';
|
|
68
|
+
|
|
69
|
+
const result = await sdjwt.safeVerify(credential);
|
|
70
|
+
|
|
71
|
+
if (result.success) {
|
|
72
|
+
// Verification succeeded
|
|
73
|
+
console.log('Verified payload:', result.data.payload);
|
|
74
|
+
console.log('Header:', result.data.header);
|
|
75
|
+
if (result.data.kb) {
|
|
76
|
+
console.log('Key binding:', result.data.kb);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
// Verification failed - inspect all errors
|
|
80
|
+
for (const error of result.errors) {
|
|
81
|
+
console.error(`[${error.code}] ${error.message}`);
|
|
82
|
+
if (error.details) {
|
|
83
|
+
console.error('Details:', error.details);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
##### Error Codes
|
|
90
|
+
|
|
91
|
+
The `safeVerify()` method returns errors with the following codes:
|
|
92
|
+
|
|
93
|
+
| Code | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `HASHER_NOT_FOUND` | Hasher function not configured |
|
|
96
|
+
| `VERIFIER_NOT_FOUND` | Verifier function not configured |
|
|
97
|
+
| `INVALID_SD_JWT` | SD-JWT structure is invalid or cannot be decoded |
|
|
98
|
+
| `INVALID_JWT_FORMAT` | JWT format is malformed |
|
|
99
|
+
| `JWT_NOT_YET_VALID` | JWT `iat` or `nbf` claim is in the future |
|
|
100
|
+
| `JWT_EXPIRED` | JWT `exp` claim is in the past |
|
|
101
|
+
| `INVALID_JWT_SIGNATURE` | Signature verification failed |
|
|
102
|
+
| `MISSING_REQUIRED_CLAIMS` | Required claim keys are not present |
|
|
103
|
+
| `KEY_BINDING_JWT_MISSING` | Key binding JWT required but not present |
|
|
104
|
+
| `KEY_BINDING_VERIFIER_NOT_FOUND` | Key binding verifier not configured |
|
|
105
|
+
| `KEY_BINDING_SIGNATURE_INVALID` | Key binding signature verification failed |
|
|
106
|
+
| `KEY_BINDING_SD_HASH_INVALID` | Key binding `sd_hash` does not match |
|
|
107
|
+
| `UNKNOWN_ERROR` | An unexpected error occurred |
|
|
108
|
+
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _sd_jwt_types from '@sd-jwt/types';
|
|
2
|
-
import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions } from '@sd-jwt/types';
|
|
2
|
+
import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions, SafeVerifyResult } from '@sd-jwt/types';
|
|
3
3
|
import { Disclosure } from '@sd-jwt/utils';
|
|
4
4
|
|
|
5
5
|
type FlattenJSONData = {
|
|
@@ -230,6 +230,22 @@ declare class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
230
230
|
header: _sd_jwt_types.kbHeader;
|
|
231
231
|
};
|
|
232
232
|
}>;
|
|
233
|
+
/**
|
|
234
|
+
* Safe verification that collects all errors instead of failing fast.
|
|
235
|
+
* Returns a result object with either the verified data or an array of all errors.
|
|
236
|
+
*
|
|
237
|
+
* @param encodedSDJwt - The encoded SD-JWT to verify
|
|
238
|
+
* @param options - Verification options
|
|
239
|
+
* @returns A SafeVerifyResult containing either success data or collected errors
|
|
240
|
+
*/
|
|
241
|
+
safeVerify(encodedSDJwt: string, options?: T & VerifierOptions): Promise<SafeVerifyResult<{
|
|
242
|
+
payload: ExtendedPayload;
|
|
243
|
+
header: Record<string, unknown> | undefined;
|
|
244
|
+
kb?: {
|
|
245
|
+
payload: Record<string, unknown>;
|
|
246
|
+
header: Record<string, unknown>;
|
|
247
|
+
};
|
|
248
|
+
}>>;
|
|
233
249
|
private calculateSDHash;
|
|
234
250
|
/**
|
|
235
251
|
* This function is for validating the SD JWT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _sd_jwt_types from '@sd-jwt/types';
|
|
2
|
-
import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions } from '@sd-jwt/types';
|
|
2
|
+
import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions, SafeVerifyResult } from '@sd-jwt/types';
|
|
3
3
|
import { Disclosure } from '@sd-jwt/utils';
|
|
4
4
|
|
|
5
5
|
type FlattenJSONData = {
|
|
@@ -230,6 +230,22 @@ declare class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
230
230
|
header: _sd_jwt_types.kbHeader;
|
|
231
231
|
};
|
|
232
232
|
}>;
|
|
233
|
+
/**
|
|
234
|
+
* Safe verification that collects all errors instead of failing fast.
|
|
235
|
+
* Returns a result object with either the verified data or an array of all errors.
|
|
236
|
+
*
|
|
237
|
+
* @param encodedSDJwt - The encoded SD-JWT to verify
|
|
238
|
+
* @param options - Verification options
|
|
239
|
+
* @returns A SafeVerifyResult containing either success data or collected errors
|
|
240
|
+
*/
|
|
241
|
+
safeVerify(encodedSDJwt: string, options?: T & VerifierOptions): Promise<SafeVerifyResult<{
|
|
242
|
+
payload: ExtendedPayload;
|
|
243
|
+
header: Record<string, unknown> | undefined;
|
|
244
|
+
kb?: {
|
|
245
|
+
payload: Record<string, unknown>;
|
|
246
|
+
header: Record<string, unknown>;
|
|
247
|
+
};
|
|
248
|
+
}>>;
|
|
233
249
|
private calculateSDHash;
|
|
234
250
|
/**
|
|
235
251
|
* This function is for validating the SD JWT
|
package/dist/index.js
CHANGED
|
@@ -818,6 +818,161 @@ var _SDJwtInstance = class _SDJwtInstance {
|
|
|
818
818
|
return { payload, header, kb };
|
|
819
819
|
});
|
|
820
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Safe verification that collects all errors instead of failing fast.
|
|
823
|
+
* Returns a result object with either the verified data or an array of all errors.
|
|
824
|
+
*
|
|
825
|
+
* @param encodedSDJwt - The encoded SD-JWT to verify
|
|
826
|
+
* @param options - Verification options
|
|
827
|
+
* @returns A SafeVerifyResult containing either success data or collected errors
|
|
828
|
+
*/
|
|
829
|
+
safeVerify(encodedSDJwt, options) {
|
|
830
|
+
return __async(this, null, function* () {
|
|
831
|
+
const errors = [];
|
|
832
|
+
const addError = (code, message, details) => {
|
|
833
|
+
errors.push({ code, message, details });
|
|
834
|
+
};
|
|
835
|
+
const exceptionToCode = (error) => {
|
|
836
|
+
const message = error.message.toLowerCase();
|
|
837
|
+
if (message.includes("hasher not found")) return "HASHER_NOT_FOUND";
|
|
838
|
+
if (message.includes("verifier not found")) return "VERIFIER_NOT_FOUND";
|
|
839
|
+
if (message.includes("invalid sd jwt") || message.includes("invalid jwt"))
|
|
840
|
+
return "INVALID_SD_JWT";
|
|
841
|
+
if (message.includes("not yet valid")) return "JWT_NOT_YET_VALID";
|
|
842
|
+
if (message.includes("expired")) return "JWT_EXPIRED";
|
|
843
|
+
if (message.includes("signature")) return "INVALID_JWT_SIGNATURE";
|
|
844
|
+
if (message.includes("missing required claim"))
|
|
845
|
+
return "MISSING_REQUIRED_CLAIMS";
|
|
846
|
+
if (message.includes("key binding jwt not exist"))
|
|
847
|
+
return "KEY_BINDING_JWT_MISSING";
|
|
848
|
+
if (message.includes("key binding verifier not found"))
|
|
849
|
+
return "KEY_BINDING_VERIFIER_NOT_FOUND";
|
|
850
|
+
if (message.includes("sd_hash")) return "KEY_BINDING_SD_HASH_INVALID";
|
|
851
|
+
return "UNKNOWN_ERROR";
|
|
852
|
+
};
|
|
853
|
+
if (!this.userConfig.hasher) {
|
|
854
|
+
addError("HASHER_NOT_FOUND", "Hasher not found");
|
|
855
|
+
}
|
|
856
|
+
if (!this.userConfig.verifier) {
|
|
857
|
+
addError("VERIFIER_NOT_FOUND", "Verifier not found");
|
|
858
|
+
}
|
|
859
|
+
if (errors.length > 0) {
|
|
860
|
+
return { success: false, errors };
|
|
861
|
+
}
|
|
862
|
+
const hasher = this.userConfig.hasher;
|
|
863
|
+
let sdjwt;
|
|
864
|
+
let payload;
|
|
865
|
+
let header;
|
|
866
|
+
try {
|
|
867
|
+
sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
868
|
+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
869
|
+
addError("INVALID_SD_JWT", "Invalid SD JWT: missing JWT or payload");
|
|
870
|
+
}
|
|
871
|
+
} catch (error) {
|
|
872
|
+
addError(
|
|
873
|
+
"INVALID_SD_JWT",
|
|
874
|
+
`Failed to decode SD-JWT: ${error.message}`,
|
|
875
|
+
error
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (sdjwt == null ? void 0 : sdjwt.jwt) {
|
|
879
|
+
try {
|
|
880
|
+
const result = yield this.VerifyJwt(sdjwt.jwt, options);
|
|
881
|
+
header = result.header;
|
|
882
|
+
const claims = yield sdjwt.getClaims(hasher);
|
|
883
|
+
payload = claims;
|
|
884
|
+
} catch (error) {
|
|
885
|
+
const code = exceptionToCode(error);
|
|
886
|
+
addError(code, error.message, error);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (sdjwt && (options == null ? void 0 : options.requiredClaimKeys)) {
|
|
890
|
+
try {
|
|
891
|
+
const keys = yield sdjwt.keys(hasher);
|
|
892
|
+
const missingKeys = options.requiredClaimKeys.filter(
|
|
893
|
+
(k) => !keys.includes(k)
|
|
894
|
+
);
|
|
895
|
+
if (missingKeys.length > 0) {
|
|
896
|
+
addError(
|
|
897
|
+
"MISSING_REQUIRED_CLAIMS",
|
|
898
|
+
`Missing required claim keys: ${missingKeys.join(", ")}`,
|
|
899
|
+
{ missingKeys }
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
addError(
|
|
904
|
+
"UNKNOWN_ERROR",
|
|
905
|
+
`Failed to check required claims: ${error.message}`,
|
|
906
|
+
error
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
let kb;
|
|
911
|
+
if ((options == null ? void 0 : options.keyBindingNonce) && sdjwt) {
|
|
912
|
+
if (!sdjwt.kbJwt) {
|
|
913
|
+
addError("KEY_BINDING_JWT_MISSING", "Key Binding JWT not exist");
|
|
914
|
+
} else if (!this.userConfig.kbVerifier) {
|
|
915
|
+
addError(
|
|
916
|
+
"KEY_BINDING_VERIFIER_NOT_FOUND",
|
|
917
|
+
"Key Binding Verifier not found"
|
|
918
|
+
);
|
|
919
|
+
} else if (payload) {
|
|
920
|
+
try {
|
|
921
|
+
const kbResult = yield sdjwt.kbJwt.verifyKB({
|
|
922
|
+
verifier: this.userConfig.kbVerifier,
|
|
923
|
+
payload,
|
|
924
|
+
nonce: options.keyBindingNonce
|
|
925
|
+
});
|
|
926
|
+
if (!kbResult) {
|
|
927
|
+
addError(
|
|
928
|
+
"KEY_BINDING_SIGNATURE_INVALID",
|
|
929
|
+
"Key binding signature is not valid"
|
|
930
|
+
);
|
|
931
|
+
} else {
|
|
932
|
+
kb = kbResult;
|
|
933
|
+
const sdjwtWithoutKb = new SDJwt({
|
|
934
|
+
jwt: sdjwt.jwt,
|
|
935
|
+
disclosures: sdjwt.disclosures
|
|
936
|
+
});
|
|
937
|
+
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
|
|
938
|
+
const sdHashStr = yield this.calculateSDHash(
|
|
939
|
+
presentSdJwtWithoutKb,
|
|
940
|
+
sdjwt,
|
|
941
|
+
hasher
|
|
942
|
+
);
|
|
943
|
+
if (sdHashStr !== kbResult.payload.sd_hash) {
|
|
944
|
+
addError(
|
|
945
|
+
"KEY_BINDING_SD_HASH_INVALID",
|
|
946
|
+
"Invalid sd_hash in Key Binding JWT",
|
|
947
|
+
{
|
|
948
|
+
expected: sdHashStr,
|
|
949
|
+
received: kbResult.payload.sd_hash
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} catch (error) {
|
|
955
|
+
addError(
|
|
956
|
+
"KEY_BINDING_SIGNATURE_INVALID",
|
|
957
|
+
`Key binding verification failed: ${error.message}`,
|
|
958
|
+
error
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (errors.length > 0) {
|
|
964
|
+
return { success: false, errors };
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
success: true,
|
|
968
|
+
data: {
|
|
969
|
+
payload,
|
|
970
|
+
header,
|
|
971
|
+
kb
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
}
|
|
821
976
|
calculateSDHash(presentSdJwtWithoutKb, sdjwt, hasher) {
|
|
822
977
|
return __async(this, null, function* () {
|
|
823
978
|
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
package/dist/index.mjs
CHANGED
|
@@ -803,6 +803,161 @@ var _SDJwtInstance = class _SDJwtInstance {
|
|
|
803
803
|
return { payload, header, kb };
|
|
804
804
|
});
|
|
805
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Safe verification that collects all errors instead of failing fast.
|
|
808
|
+
* Returns a result object with either the verified data or an array of all errors.
|
|
809
|
+
*
|
|
810
|
+
* @param encodedSDJwt - The encoded SD-JWT to verify
|
|
811
|
+
* @param options - Verification options
|
|
812
|
+
* @returns A SafeVerifyResult containing either success data or collected errors
|
|
813
|
+
*/
|
|
814
|
+
safeVerify(encodedSDJwt, options) {
|
|
815
|
+
return __async(this, null, function* () {
|
|
816
|
+
const errors = [];
|
|
817
|
+
const addError = (code, message, details) => {
|
|
818
|
+
errors.push({ code, message, details });
|
|
819
|
+
};
|
|
820
|
+
const exceptionToCode = (error) => {
|
|
821
|
+
const message = error.message.toLowerCase();
|
|
822
|
+
if (message.includes("hasher not found")) return "HASHER_NOT_FOUND";
|
|
823
|
+
if (message.includes("verifier not found")) return "VERIFIER_NOT_FOUND";
|
|
824
|
+
if (message.includes("invalid sd jwt") || message.includes("invalid jwt"))
|
|
825
|
+
return "INVALID_SD_JWT";
|
|
826
|
+
if (message.includes("not yet valid")) return "JWT_NOT_YET_VALID";
|
|
827
|
+
if (message.includes("expired")) return "JWT_EXPIRED";
|
|
828
|
+
if (message.includes("signature")) return "INVALID_JWT_SIGNATURE";
|
|
829
|
+
if (message.includes("missing required claim"))
|
|
830
|
+
return "MISSING_REQUIRED_CLAIMS";
|
|
831
|
+
if (message.includes("key binding jwt not exist"))
|
|
832
|
+
return "KEY_BINDING_JWT_MISSING";
|
|
833
|
+
if (message.includes("key binding verifier not found"))
|
|
834
|
+
return "KEY_BINDING_VERIFIER_NOT_FOUND";
|
|
835
|
+
if (message.includes("sd_hash")) return "KEY_BINDING_SD_HASH_INVALID";
|
|
836
|
+
return "UNKNOWN_ERROR";
|
|
837
|
+
};
|
|
838
|
+
if (!this.userConfig.hasher) {
|
|
839
|
+
addError("HASHER_NOT_FOUND", "Hasher not found");
|
|
840
|
+
}
|
|
841
|
+
if (!this.userConfig.verifier) {
|
|
842
|
+
addError("VERIFIER_NOT_FOUND", "Verifier not found");
|
|
843
|
+
}
|
|
844
|
+
if (errors.length > 0) {
|
|
845
|
+
return { success: false, errors };
|
|
846
|
+
}
|
|
847
|
+
const hasher = this.userConfig.hasher;
|
|
848
|
+
let sdjwt;
|
|
849
|
+
let payload;
|
|
850
|
+
let header;
|
|
851
|
+
try {
|
|
852
|
+
sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
853
|
+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
854
|
+
addError("INVALID_SD_JWT", "Invalid SD JWT: missing JWT or payload");
|
|
855
|
+
}
|
|
856
|
+
} catch (error) {
|
|
857
|
+
addError(
|
|
858
|
+
"INVALID_SD_JWT",
|
|
859
|
+
`Failed to decode SD-JWT: ${error.message}`,
|
|
860
|
+
error
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (sdjwt == null ? void 0 : sdjwt.jwt) {
|
|
864
|
+
try {
|
|
865
|
+
const result = yield this.VerifyJwt(sdjwt.jwt, options);
|
|
866
|
+
header = result.header;
|
|
867
|
+
const claims = yield sdjwt.getClaims(hasher);
|
|
868
|
+
payload = claims;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
const code = exceptionToCode(error);
|
|
871
|
+
addError(code, error.message, error);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (sdjwt && (options == null ? void 0 : options.requiredClaimKeys)) {
|
|
875
|
+
try {
|
|
876
|
+
const keys = yield sdjwt.keys(hasher);
|
|
877
|
+
const missingKeys = options.requiredClaimKeys.filter(
|
|
878
|
+
(k) => !keys.includes(k)
|
|
879
|
+
);
|
|
880
|
+
if (missingKeys.length > 0) {
|
|
881
|
+
addError(
|
|
882
|
+
"MISSING_REQUIRED_CLAIMS",
|
|
883
|
+
`Missing required claim keys: ${missingKeys.join(", ")}`,
|
|
884
|
+
{ missingKeys }
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
} catch (error) {
|
|
888
|
+
addError(
|
|
889
|
+
"UNKNOWN_ERROR",
|
|
890
|
+
`Failed to check required claims: ${error.message}`,
|
|
891
|
+
error
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
let kb;
|
|
896
|
+
if ((options == null ? void 0 : options.keyBindingNonce) && sdjwt) {
|
|
897
|
+
if (!sdjwt.kbJwt) {
|
|
898
|
+
addError("KEY_BINDING_JWT_MISSING", "Key Binding JWT not exist");
|
|
899
|
+
} else if (!this.userConfig.kbVerifier) {
|
|
900
|
+
addError(
|
|
901
|
+
"KEY_BINDING_VERIFIER_NOT_FOUND",
|
|
902
|
+
"Key Binding Verifier not found"
|
|
903
|
+
);
|
|
904
|
+
} else if (payload) {
|
|
905
|
+
try {
|
|
906
|
+
const kbResult = yield sdjwt.kbJwt.verifyKB({
|
|
907
|
+
verifier: this.userConfig.kbVerifier,
|
|
908
|
+
payload,
|
|
909
|
+
nonce: options.keyBindingNonce
|
|
910
|
+
});
|
|
911
|
+
if (!kbResult) {
|
|
912
|
+
addError(
|
|
913
|
+
"KEY_BINDING_SIGNATURE_INVALID",
|
|
914
|
+
"Key binding signature is not valid"
|
|
915
|
+
);
|
|
916
|
+
} else {
|
|
917
|
+
kb = kbResult;
|
|
918
|
+
const sdjwtWithoutKb = new SDJwt({
|
|
919
|
+
jwt: sdjwt.jwt,
|
|
920
|
+
disclosures: sdjwt.disclosures
|
|
921
|
+
});
|
|
922
|
+
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
|
|
923
|
+
const sdHashStr = yield this.calculateSDHash(
|
|
924
|
+
presentSdJwtWithoutKb,
|
|
925
|
+
sdjwt,
|
|
926
|
+
hasher
|
|
927
|
+
);
|
|
928
|
+
if (sdHashStr !== kbResult.payload.sd_hash) {
|
|
929
|
+
addError(
|
|
930
|
+
"KEY_BINDING_SD_HASH_INVALID",
|
|
931
|
+
"Invalid sd_hash in Key Binding JWT",
|
|
932
|
+
{
|
|
933
|
+
expected: sdHashStr,
|
|
934
|
+
received: kbResult.payload.sd_hash
|
|
935
|
+
}
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} catch (error) {
|
|
940
|
+
addError(
|
|
941
|
+
"KEY_BINDING_SIGNATURE_INVALID",
|
|
942
|
+
`Key binding verification failed: ${error.message}`,
|
|
943
|
+
error
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (errors.length > 0) {
|
|
949
|
+
return { success: false, errors };
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
success: true,
|
|
953
|
+
data: {
|
|
954
|
+
payload,
|
|
955
|
+
header,
|
|
956
|
+
kb
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
});
|
|
960
|
+
}
|
|
806
961
|
calculateSDHash(presentSdJwtWithoutKb, sdjwt, hasher) {
|
|
807
962
|
return __async(this, null, function* () {
|
|
808
963
|
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sd-jwt/core",
|
|
3
|
-
"version": "0.19.1-next.
|
|
3
|
+
"version": "0.19.1-next.2+c96a9a0",
|
|
4
4
|
"description": "sd-jwt draft 7 implementation in typescript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@sd-jwt/crypto-nodejs": "0.19.1-next.
|
|
40
|
+
"@sd-jwt/crypto-nodejs": "0.19.1-next.2+c96a9a0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@sd-jwt/decode": "0.19.1-next.
|
|
44
|
-
"@sd-jwt/present": "0.19.1-next.
|
|
45
|
-
"@sd-jwt/types": "0.19.1-next.
|
|
46
|
-
"@sd-jwt/utils": "0.19.1-next.
|
|
43
|
+
"@sd-jwt/decode": "0.19.1-next.2+c96a9a0",
|
|
44
|
+
"@sd-jwt/present": "0.19.1-next.2+c96a9a0",
|
|
45
|
+
"@sd-jwt/types": "0.19.1-next.2+c96a9a0",
|
|
46
|
+
"@sd-jwt/utils": "0.19.1-next.2+c96a9a0"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"esm"
|
|
62
62
|
]
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "c96a9a0e73d09d2978db5004a3f7ca307bb926f3"
|
|
65
65
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
KB_JWT_TYP,
|
|
8
8
|
type KBOptions,
|
|
9
9
|
type PresentationFrame,
|
|
10
|
+
type SafeVerifyResult,
|
|
10
11
|
type SDJWTCompact,
|
|
11
12
|
type SDJWTConfig,
|
|
12
13
|
type Signer,
|
|
14
|
+
type VerificationError,
|
|
15
|
+
type VerificationErrorCode,
|
|
13
16
|
} from '@sd-jwt/types';
|
|
14
17
|
import {
|
|
15
18
|
base64urlDecode,
|
|
@@ -259,6 +262,202 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
259
262
|
return { payload, header, kb };
|
|
260
263
|
}
|
|
261
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Safe verification that collects all errors instead of failing fast.
|
|
267
|
+
* Returns a result object with either the verified data or an array of all errors.
|
|
268
|
+
*
|
|
269
|
+
* @param encodedSDJwt - The encoded SD-JWT to verify
|
|
270
|
+
* @param options - Verification options
|
|
271
|
+
* @returns A SafeVerifyResult containing either success data or collected errors
|
|
272
|
+
*/
|
|
273
|
+
public async safeVerify(
|
|
274
|
+
encodedSDJwt: string,
|
|
275
|
+
options?: T & VerifierOptions,
|
|
276
|
+
): Promise<
|
|
277
|
+
SafeVerifyResult<{
|
|
278
|
+
payload: ExtendedPayload;
|
|
279
|
+
header: Record<string, unknown> | undefined;
|
|
280
|
+
kb?: {
|
|
281
|
+
payload: Record<string, unknown>;
|
|
282
|
+
header: Record<string, unknown>;
|
|
283
|
+
};
|
|
284
|
+
}>
|
|
285
|
+
> {
|
|
286
|
+
const errors: VerificationError[] = [];
|
|
287
|
+
|
|
288
|
+
// Helper to add errors
|
|
289
|
+
const addError = (
|
|
290
|
+
code: VerificationErrorCode,
|
|
291
|
+
message: string,
|
|
292
|
+
details?: unknown,
|
|
293
|
+
) => {
|
|
294
|
+
errors.push({ code, message, details });
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Helper to convert exception to error code
|
|
298
|
+
const exceptionToCode = (error: Error): VerificationErrorCode => {
|
|
299
|
+
const message = error.message.toLowerCase();
|
|
300
|
+
if (message.includes('hasher not found')) return 'HASHER_NOT_FOUND';
|
|
301
|
+
if (message.includes('verifier not found')) return 'VERIFIER_NOT_FOUND';
|
|
302
|
+
if (message.includes('invalid sd jwt') || message.includes('invalid jwt'))
|
|
303
|
+
return 'INVALID_SD_JWT';
|
|
304
|
+
if (message.includes('not yet valid')) return 'JWT_NOT_YET_VALID';
|
|
305
|
+
if (message.includes('expired')) return 'JWT_EXPIRED';
|
|
306
|
+
if (message.includes('signature')) return 'INVALID_JWT_SIGNATURE';
|
|
307
|
+
if (message.includes('missing required claim'))
|
|
308
|
+
return 'MISSING_REQUIRED_CLAIMS';
|
|
309
|
+
if (message.includes('key binding jwt not exist'))
|
|
310
|
+
return 'KEY_BINDING_JWT_MISSING';
|
|
311
|
+
if (message.includes('key binding verifier not found'))
|
|
312
|
+
return 'KEY_BINDING_VERIFIER_NOT_FOUND';
|
|
313
|
+
if (message.includes('sd_hash')) return 'KEY_BINDING_SD_HASH_INVALID';
|
|
314
|
+
return 'UNKNOWN_ERROR';
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Check basic configuration first
|
|
318
|
+
if (!this.userConfig.hasher) {
|
|
319
|
+
addError('HASHER_NOT_FOUND', 'Hasher not found');
|
|
320
|
+
}
|
|
321
|
+
if (!this.userConfig.verifier) {
|
|
322
|
+
addError('VERIFIER_NOT_FOUND', 'Verifier not found');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If basic config is missing, return early
|
|
326
|
+
if (errors.length > 0) {
|
|
327
|
+
return { success: false, errors };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const hasher = this.userConfig.hasher as Hasher;
|
|
331
|
+
|
|
332
|
+
// Try to decode and validate the SD-JWT
|
|
333
|
+
let sdjwt: SDJwt | undefined;
|
|
334
|
+
let payload: ExtendedPayload | undefined;
|
|
335
|
+
let header: Record<string, unknown> | undefined;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
339
|
+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
340
|
+
addError('INVALID_SD_JWT', 'Invalid SD JWT: missing JWT or payload');
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
addError(
|
|
344
|
+
'INVALID_SD_JWT',
|
|
345
|
+
`Failed to decode SD-JWT: ${(error as Error).message}`,
|
|
346
|
+
error,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Validate signature and claims
|
|
351
|
+
if (sdjwt?.jwt) {
|
|
352
|
+
try {
|
|
353
|
+
const result = await this.VerifyJwt(sdjwt.jwt, options);
|
|
354
|
+
header = result.header;
|
|
355
|
+
const claims = await sdjwt.getClaims(hasher);
|
|
356
|
+
payload = claims as ExtendedPayload;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const code = exceptionToCode(error as Error);
|
|
359
|
+
addError(code, (error as Error).message, error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check required claim keys
|
|
364
|
+
if (sdjwt && options?.requiredClaimKeys) {
|
|
365
|
+
try {
|
|
366
|
+
const keys = await sdjwt.keys(hasher);
|
|
367
|
+
const missingKeys = options.requiredClaimKeys.filter(
|
|
368
|
+
(k) => !keys.includes(k),
|
|
369
|
+
);
|
|
370
|
+
if (missingKeys.length > 0) {
|
|
371
|
+
addError(
|
|
372
|
+
'MISSING_REQUIRED_CLAIMS',
|
|
373
|
+
`Missing required claim keys: ${missingKeys.join(', ')}`,
|
|
374
|
+
{ missingKeys },
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
addError(
|
|
379
|
+
'UNKNOWN_ERROR',
|
|
380
|
+
`Failed to check required claims: ${(error as Error).message}`,
|
|
381
|
+
error,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Verify key binding if requested
|
|
387
|
+
let kb:
|
|
388
|
+
| { payload: Record<string, unknown>; header: Record<string, unknown> }
|
|
389
|
+
| undefined;
|
|
390
|
+
if (options?.keyBindingNonce && sdjwt) {
|
|
391
|
+
if (!sdjwt.kbJwt) {
|
|
392
|
+
addError('KEY_BINDING_JWT_MISSING', 'Key Binding JWT not exist');
|
|
393
|
+
} else if (!this.userConfig.kbVerifier) {
|
|
394
|
+
addError(
|
|
395
|
+
'KEY_BINDING_VERIFIER_NOT_FOUND',
|
|
396
|
+
'Key Binding Verifier not found',
|
|
397
|
+
);
|
|
398
|
+
} else if (payload) {
|
|
399
|
+
try {
|
|
400
|
+
const kbResult = await sdjwt.kbJwt.verifyKB({
|
|
401
|
+
verifier: this.userConfig.kbVerifier,
|
|
402
|
+
payload: payload as JwtPayload,
|
|
403
|
+
nonce: options.keyBindingNonce,
|
|
404
|
+
});
|
|
405
|
+
if (!kbResult) {
|
|
406
|
+
addError(
|
|
407
|
+
'KEY_BINDING_SIGNATURE_INVALID',
|
|
408
|
+
'Key binding signature is not valid',
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
kb = kbResult;
|
|
412
|
+
|
|
413
|
+
// Verify sd_hash
|
|
414
|
+
const sdjwtWithoutKb = new SDJwt({
|
|
415
|
+
jwt: sdjwt.jwt,
|
|
416
|
+
disclosures: sdjwt.disclosures,
|
|
417
|
+
});
|
|
418
|
+
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
|
|
419
|
+
const sdHashStr = await this.calculateSDHash(
|
|
420
|
+
presentSdJwtWithoutKb,
|
|
421
|
+
sdjwt,
|
|
422
|
+
hasher,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
if (sdHashStr !== kbResult.payload.sd_hash) {
|
|
426
|
+
addError(
|
|
427
|
+
'KEY_BINDING_SD_HASH_INVALID',
|
|
428
|
+
'Invalid sd_hash in Key Binding JWT',
|
|
429
|
+
{
|
|
430
|
+
expected: sdHashStr,
|
|
431
|
+
received: kbResult.payload.sd_hash,
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
addError(
|
|
438
|
+
'KEY_BINDING_SIGNATURE_INVALID',
|
|
439
|
+
`Key binding verification failed: ${(error as Error).message}`,
|
|
440
|
+
error,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Return result
|
|
447
|
+
if (errors.length > 0) {
|
|
448
|
+
return { success: false, errors };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
data: {
|
|
454
|
+
payload: payload as ExtendedPayload,
|
|
455
|
+
header,
|
|
456
|
+
kb,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
262
461
|
private async calculateSDHash(
|
|
263
462
|
presentSdJwtWithoutKb: string,
|
|
264
463
|
sdjwt: SDJwt,
|
package/src/test/index.spec.ts
CHANGED
|
@@ -627,4 +627,156 @@ describe('index', () => {
|
|
|
627
627
|
expect(decode).toBeDefined();
|
|
628
628
|
},
|
|
629
629
|
);
|
|
630
|
+
|
|
631
|
+
test('safeVerify - success case', async () => {
|
|
632
|
+
const { signer, verifier } = createSignerVerifier();
|
|
633
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
634
|
+
signer,
|
|
635
|
+
signAlg: 'EdDSA',
|
|
636
|
+
verifier,
|
|
637
|
+
hasher: digest,
|
|
638
|
+
saltGenerator: generateSalt,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const credential = await sdjwt.issue(
|
|
642
|
+
{
|
|
643
|
+
foo: 'bar',
|
|
644
|
+
iss: 'Issuer',
|
|
645
|
+
iat: Math.floor(Date.now() / 1000),
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
_sd: ['foo'],
|
|
649
|
+
},
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
const result = await sdjwt.safeVerify(credential);
|
|
653
|
+
|
|
654
|
+
expect(result.success).toBe(true);
|
|
655
|
+
if (result.success) {
|
|
656
|
+
expect(result.data.payload).toBeDefined();
|
|
657
|
+
expect(result.data.payload.foo).toBe('bar');
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('safeVerify - collect multiple errors', async () => {
|
|
662
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({});
|
|
663
|
+
|
|
664
|
+
const result = await sdjwt.safeVerify('invalid.jwt.token');
|
|
665
|
+
|
|
666
|
+
expect(result.success).toBe(false);
|
|
667
|
+
if (!result.success) {
|
|
668
|
+
// Should have multiple errors: hasher not found, verifier not found
|
|
669
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(2);
|
|
670
|
+
const errorCodes = result.errors.map((e) => e.code);
|
|
671
|
+
expect(errorCodes).toContain('HASHER_NOT_FOUND');
|
|
672
|
+
expect(errorCodes).toContain('VERIFIER_NOT_FOUND');
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('safeVerify - invalid signature error', async () => {
|
|
677
|
+
const { signer } = createSignerVerifier();
|
|
678
|
+
const { verifier: wrongVerifier } = createSignerVerifier(); // Different key pair
|
|
679
|
+
|
|
680
|
+
const issuer = new SDJwtInstance<SdJwtPayload>({
|
|
681
|
+
signer,
|
|
682
|
+
signAlg: 'EdDSA',
|
|
683
|
+
hasher: digest,
|
|
684
|
+
saltGenerator: generateSalt,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const verifierInstance = new SDJwtInstance<SdJwtPayload>({
|
|
688
|
+
verifier: wrongVerifier,
|
|
689
|
+
hasher: digest,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const credential = await issuer.issue(
|
|
693
|
+
{
|
|
694
|
+
foo: 'bar',
|
|
695
|
+
iss: 'Issuer',
|
|
696
|
+
iat: Math.floor(Date.now() / 1000),
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
_sd: ['foo'],
|
|
700
|
+
},
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const result = await verifierInstance.safeVerify(credential);
|
|
704
|
+
|
|
705
|
+
expect(result.success).toBe(false);
|
|
706
|
+
if (!result.success) {
|
|
707
|
+
// The error message from the verifier contains 'signature' which maps to INVALID_JWT_SIGNATURE
|
|
708
|
+
const hasSignatureError = result.errors.some(
|
|
709
|
+
(e) =>
|
|
710
|
+
e.code === 'INVALID_JWT_SIGNATURE' ||
|
|
711
|
+
e.message.toLowerCase().includes('signature'),
|
|
712
|
+
);
|
|
713
|
+
expect(hasSignatureError).toBe(true);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test('safeVerify - expired JWT error', async () => {
|
|
718
|
+
const { signer, verifier } = createSignerVerifier();
|
|
719
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
720
|
+
signer,
|
|
721
|
+
signAlg: 'EdDSA',
|
|
722
|
+
verifier,
|
|
723
|
+
hasher: digest,
|
|
724
|
+
saltGenerator: generateSalt,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const credential = await sdjwt.issue(
|
|
728
|
+
{
|
|
729
|
+
foo: 'bar',
|
|
730
|
+
iss: 'Issuer',
|
|
731
|
+
iat: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
|
|
732
|
+
exp: Math.floor(Date.now() / 1000) - 1800, // Expired 30 min ago
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
_sd: ['foo'],
|
|
736
|
+
},
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const result = await sdjwt.safeVerify(credential);
|
|
740
|
+
|
|
741
|
+
expect(result.success).toBe(false);
|
|
742
|
+
if (!result.success) {
|
|
743
|
+
expect(result.errors.some((e) => e.code === 'JWT_EXPIRED')).toBe(true);
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test('safeVerify - missing required claims error', async () => {
|
|
748
|
+
const { signer, verifier } = createSignerVerifier();
|
|
749
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
750
|
+
signer,
|
|
751
|
+
signAlg: 'EdDSA',
|
|
752
|
+
verifier,
|
|
753
|
+
hasher: digest,
|
|
754
|
+
saltGenerator: generateSalt,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const credential = await sdjwt.issue(
|
|
758
|
+
{
|
|
759
|
+
foo: 'bar',
|
|
760
|
+
iss: 'Issuer',
|
|
761
|
+
iat: Math.floor(Date.now() / 1000),
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
_sd: ['foo'],
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// Present without disclosing 'foo'
|
|
769
|
+
const presentation = await sdjwt.present(credential, {});
|
|
770
|
+
|
|
771
|
+
const result = await sdjwt.safeVerify(presentation, {
|
|
772
|
+
requiredClaimKeys: ['foo', 'missing_claim'],
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
expect(result.success).toBe(false);
|
|
776
|
+
if (!result.success) {
|
|
777
|
+
expect(
|
|
778
|
+
result.errors.some((e) => e.code === 'MISSING_REQUIRED_CLAIMS'),
|
|
779
|
+
).toBe(true);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
630
782
|
});
|