sealed-lattice 0.0.9 → 0.0.11
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 +9 -100
- package/dist/index.d.ts +18 -1
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -0
- package/dist/internal/crypto/index.js +450 -0
- package/dist/internal/election-foundation/board/index.js +322 -0
- package/dist/internal/election-foundation/closing/index.js +255 -0
- package/dist/internal/election-foundation/common/digests.js +1 -0
- package/dist/internal/election-foundation/common/signatures.js +1 -0
- package/dist/internal/election-foundation/common/verification-helpers.js +9 -0
- package/dist/internal/election-foundation/finality/index.js +305 -0
- package/dist/internal/election-foundation/index.js +15 -0
- package/dist/internal/election-foundation/lifecycle/capabilities.js +233 -0
- package/dist/internal/election-foundation/lifecycle/labels.js +186 -0
- package/dist/internal/election-foundation/lifecycle/lifecycle.js +57 -0
- package/dist/internal/election-foundation/lifecycle/poll-spec.js +126 -0
- package/dist/internal/election-foundation/lifecycle/profiles.js +13 -0
- package/dist/internal/election-foundation/lifecycle/refusal.js +9 -0
- package/dist/internal/election-foundation/lifecycle/thresholds.js +105 -0
- package/dist/internal/election-foundation/ordering/index.js +115 -0
- package/dist/internal/election-foundation/recovery/index.js +241 -0
- package/dist/internal/election-foundation/roster/index.js +447 -0
- package/dist/internal/election-foundation/target-phase/index.js +366 -0
- package/dist/internal/transcript-core-bridge.js +194 -0
- package/dist/internal/types.d.ts +720 -0
- package/dist/internal/types.js +24 -0
- package/dist/kernel.d.ts +6 -0
- package/dist/kernel.js +4 -0
- package/dist/kernel.js.map +1 -0
- package/dist/sealed-lattice-kernel.wasm +0 -0
- package/package.json +23 -78
- package/dist/core/bytes.d.ts +0 -2
- package/dist/core/bytes.js +0 -8
- package/dist/core/crypto.d.ts +0 -10
- package/dist/core/crypto.js +0 -18
- package/dist/core/errors.d.ts +0 -7
- package/dist/core/errors.js +0 -11
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.js +0 -3
package/README.md
CHANGED
|
@@ -1,102 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sealed-lattice public package
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/sealed-lattice)
|
|
3
|
+
This package is the only published npm surface in the workspace.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
[](https://nodejs.org/)
|
|
15
|
-
[](LICENSE)
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
`sealed-lattice` is a browser-native TypeScript package for post-quantum voting research prototypes.
|
|
20
|
-
|
|
21
|
-
The current implementation ships:
|
|
22
|
-
|
|
23
|
-
- a real `sha256Hex` helper on the safe root package
|
|
24
|
-
- a typed `UnsupportedRuntimeError` for missing Web Crypto support
|
|
25
|
-
- a hardened repo, docs, testing, and publish workflow around that narrow surface
|
|
26
|
-
- a deliberately narrow public surface while the lattice-native architecture is still being proven
|
|
27
|
-
|
|
28
|
-
This repository is a hardened research prototype. It is not audited production voting software.
|
|
29
|
-
|
|
30
|
-
## Release status
|
|
31
|
-
|
|
32
|
-
This repository currently tracks the initial public `sealed-lattice` surface.
|
|
33
|
-
|
|
34
|
-
The public surface is intentionally narrow while the repo, CI, docs, tests, coverage, and packaging experience are stabilized. Lattice cryptography, threshold flows, transport payloads, proofs, protocol types, and any future subpath structure are still being designed and are not frozen yet.
|
|
35
|
-
|
|
36
|
-
## Installation
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
pnpm add sealed-lattice
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Runtime requirements
|
|
43
|
-
|
|
44
|
-
- Use ESM imports such as `import { sha256Hex } from 'sealed-lattice'`. The published package does not expose CommonJS `require()` entry points.
|
|
45
|
-
- Browsers need `globalThis.crypto.subtle.digest` and `TextEncoder`.
|
|
46
|
-
- CI validates Chromium, Firefox, and WebKit on desktop, plus Chromium and WebKit in mobile emulation.
|
|
47
|
-
- Node requires version `24.14.1` or newer with `globalThis.crypto`.
|
|
48
|
-
|
|
49
|
-
## Browser support
|
|
50
|
-
|
|
51
|
-
- Use modern browsers that expose `globalThis.crypto.subtle.digest` and `TextEncoder`.
|
|
52
|
-
- Install local browser runtimes with `pnpm exec playwright install chromium firefox webkit`.
|
|
53
|
-
- Validate the current browser matrix with `pnpm run verify:browser-compat`.
|
|
54
|
-
- The local compatibility probe runs the targets supported on your current platform. CI runs the full desktop and mobile-emulation matrix on macOS.
|
|
55
|
-
|
|
56
|
-
## Safe quickstart
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import { sha256Hex } from "sealed-lattice";
|
|
60
|
-
|
|
61
|
-
const digest = await sha256Hex("sealed-lattice");
|
|
62
|
-
|
|
63
|
-
console.log(digest);
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The root package currently exposes only `sha256Hex` and `UnsupportedRuntimeError`.
|
|
67
|
-
|
|
68
|
-
## Public package boundary
|
|
69
|
-
|
|
70
|
-
- `sealed-lattice`
|
|
71
|
-
|
|
72
|
-
No additional public subpaths are promised yet. Future capability areas such as runtime helpers, serialization, transport, threshold coordination, proofs, and protocol types remain internal design space until the post-quantum flow and misuse-resistant contracts are stable.
|
|
73
|
-
|
|
74
|
-
## Documentation
|
|
75
|
-
|
|
76
|
-
- Hosted documentation site: [tenemo.github.io/sealed-lattice](https://tenemo.github.io/sealed-lattice/)
|
|
77
|
-
- Get started: [tenemo.github.io/sealed-lattice/guides/getting-started](https://tenemo.github.io/sealed-lattice/guides/getting-started/)
|
|
78
|
-
- Browser and worker usage: [tenemo.github.io/sealed-lattice/guides/browser-and-worker-usage](https://tenemo.github.io/sealed-lattice/guides/browser-and-worker-usage/)
|
|
79
|
-
- Runtime and compatibility: [tenemo.github.io/sealed-lattice/guides/runtime-and-compatibility](https://tenemo.github.io/sealed-lattice/guides/runtime-and-compatibility/)
|
|
80
|
-
- Security and non-goals: [tenemo.github.io/sealed-lattice/guides/security-and-non-goals](https://tenemo.github.io/sealed-lattice/guides/security-and-non-goals/)
|
|
81
|
-
- Protocol spec: [tenemo.github.io/sealed-lattice/spec](https://tenemo.github.io/sealed-lattice/spec/)
|
|
82
|
-
- API reference: [tenemo.github.io/sealed-lattice/api](https://tenemo.github.io/sealed-lattice/api/)
|
|
83
|
-
|
|
84
|
-
## Development
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
pnpm install
|
|
88
|
-
pnpm run lint
|
|
89
|
-
pnpm run tsc
|
|
90
|
-
pnpm exec playwright install chromium firefox webkit
|
|
91
|
-
pnpm run test
|
|
92
|
-
pnpm run verify:browser-compat
|
|
93
|
-
pnpm run verify:docs
|
|
94
|
-
pnpm run docs:build:site
|
|
95
|
-
pnpm run smoke:pack
|
|
96
|
-
pnpm run smoke:pack:npm
|
|
97
|
-
pnpm run build
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## License
|
|
101
|
-
|
|
102
|
-
This project is licensed under MPL-2.0. See [LICENSE](LICENSE).
|
|
5
|
+
The current public runtime facade exposes the safe transcript core fixture
|
|
6
|
+
verifier plus the threshold, lifecycle, poll specification, capability,
|
|
7
|
+
board-consistency, target-finality, roster-manifest, cast receipt, close
|
|
8
|
+
record, first-come ordering, and recovery-epoch helpers. It does not expose raw
|
|
9
|
+
hashing, object mutation, generic cryptography, ballots, replay-attestation
|
|
10
|
+
shell checks, semantic target acceptance, decryption-share shell checks,
|
|
11
|
+
decryption, or protocol internals.
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ActionCurrentForRecoveryEpochInput, ActionCurrentForRecoveryEpochResult, BoardConsistencyInput, BoardConsistencyVerification, CastReceiptVerification, CastReceiptVerificationInput, CapabilityContext, CapabilityDecision, CloseRecordVerification, CloseRecordVerificationInput, FirstComeOrderingInput, FirstComeOrderingVerification, LifecycleLabelInput, LifecycleLabels, LifecycleTransition, PollSpecInput, PollSpecValidation, ProtocolAction, RecoveryEpochVerification, RecoveryEpochVerificationInput, ThresholdProfile, ThresholdProfileInput, TranscriptCoreFixture, TranscriptCoreVerificationResult, RosterManifestTranscriptInput, RosterManifestTranscriptVerification, TargetFinalityVerification, TargetFinalityVerificationInput } from './internal/types.js';
|
|
2
|
+
export type { AcceptedTargetFinalityCheckpoint, ActionContext, ActionCurrentForRecoveryEpochInput, ActionCurrentForRecoveryEpochResult, AppendOnlyConsistencyProof, BaseClaimProfile, BoardConsistencyInput, BoardConsistencyVerification, CanonicalError, CanonicalErrorCode, CanonicalSignedRootObject, CapabilityContext, CapabilityDecision, CastReceipt, CastReceiptVerification, CastReceiptVerificationInput, CloseRecord, CloseRecordKind, CloseRecordVerification, CloseRecordVerificationInput, ConflictingHeadEvidence, ConflictingManifestEvidence, DuplicateBallotPolicy, ElectionManifest, EvaluationProofMode, FailureStatusLabel, FirstComeOrderingInput, FirstComeOrderingVerification, GoldenTranscriptCoreFixture, GoldenTranscriptCoreFixtureVerification, HeBackendCorruptionModel, InclusionProof, LifecycleLabelInput, LifecycleLabels, LifecycleState, LifecycleTransition, MalformedObjectFixture, MalformedObjectFixtureVerification, ManifestOpaqueBindings, ManifestPolicyDigests, MheSecurityStage, MlDsaSignatureMode, MlDsaSignatureProfile, ModeStatusLabel, PollSpec, PollSpecInput, PollSpecValidation, PollSpecValidationError, PollSpecValidationErrorCode, PrimaryStatusLabel, ProtocolAction, ProtocolDigest, ProtocolObjectType, ProtocolRefusalCode, ProtocolSignatureEnvelope, ProtocolVerificationStatusLabel, ReceiverKeyRegistration, RecoveryEpochMapEntry, RecoveryEpochUpdate, RecoveryEpochVerification, RecoveryEpochVerificationInput, RecoveryState, RefusalReason, RefusalRecord, RegistrationEntry, ResultClaimLabel, RosterManifestTranscriptInput, RosterManifestTranscriptVerification, RosterProfileKind, ScoreDomain, SignatureVerificationResult, SignedBoardHead, SignedObjectType, SignerRole, StructuredProtocolVerificationResult, TargetFinalityPolicy, TargetFinalityRecord, TargetFinalityVerification, TargetFinalityVerificationInput, ThresholdProfile, ThresholdProfileInput, ThresholdWarning, TiePolicy, TranscriptCoreAnalysis, TranscriptCoreFixture, TranscriptCoreFixtureVerification, TranscriptCoreMheSecurityStage, TranscriptCoreReplayFixture, TranscriptCoreStatusLabel, TranscriptCoreVerificationLabel, TranscriptCoreVerificationResult, TrusteeSetupEntry, ValidatedFirstComeCandidate, WitnessCheckpoint, WitnessPolicy, } from './internal/types.js';
|
|
3
|
+
export declare const deriveThresholdProfile: (input: ThresholdProfileInput) => ThresholdProfile;
|
|
4
|
+
export declare function validatePollSpec(input: PollSpecInput): PollSpecValidation;
|
|
5
|
+
export declare function validatePollSpec(input: unknown): PollSpecValidation;
|
|
6
|
+
export declare const isValidLifecycleTransition: (transition: LifecycleTransition) => boolean;
|
|
7
|
+
export declare const deriveLifecycleLabels: (input: LifecycleLabelInput) => LifecycleLabels;
|
|
8
|
+
export declare const evaluateActionCapability: (action: ProtocolAction, context: CapabilityContext) => CapabilityDecision;
|
|
9
|
+
export declare const verifyBoardConsistency: (input: BoardConsistencyInput) => BoardConsistencyVerification;
|
|
10
|
+
export declare const verifyCastReceiptShell: (input: CastReceiptVerificationInput) => CastReceiptVerification;
|
|
11
|
+
export declare const verifyCloseRecordShell: (input: CloseRecordVerificationInput) => CloseRecordVerification;
|
|
12
|
+
export declare const verifyTargetFinality: (input: TargetFinalityVerificationInput) => TargetFinalityVerification;
|
|
13
|
+
export declare const deriveValidatedFirstComeOrder: (input: FirstComeOrderingInput) => FirstComeOrderingVerification;
|
|
14
|
+
export declare const verifyFirstComePolicy: (input: FirstComeOrderingInput) => FirstComeOrderingVerification;
|
|
15
|
+
export declare const verifyRosterManifestTranscript: (input: RosterManifestTranscriptInput) => RosterManifestTranscriptVerification;
|
|
16
|
+
export declare const isActionCurrentForRecoveryEpoch: (input: ActionCurrentForRecoveryEpochInput) => ActionCurrentForRecoveryEpochResult;
|
|
17
|
+
export declare const verifyRecoveryEpochUpdate: (input: RecoveryEpochVerificationInput) => RecoveryEpochVerification;
|
|
18
|
+
export declare const verifyTranscriptCoreFixture: (fixture: TranscriptCoreFixture) => Promise<TranscriptCoreVerificationResult>;
|
package/dist/index.js
CHANGED
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
import { deriveValidatedFirstComeOrder as deriveValidatedFirstComeOrderInternal, deriveLifecycleLabels as deriveLifecycleLabelsInternal, deriveThresholdProfile as deriveThresholdProfileInternal, evaluateActionCapability as evaluateActionCapabilityInternal, verifyCastReceiptShell as verifyCastReceiptShellInternal, verifyCloseRecordShell as verifyCloseRecordShellInternal, isValidLifecycleTransition as isValidLifecycleTransitionInternal, isActionCurrentForRecoveryEpoch as isActionCurrentForRecoveryEpochInternal, validatePollSpecFromUnknown as validatePollSpecFromUnknownInternal, verifyBoardConsistency as verifyBoardConsistencyInternal, verifyFirstComePolicy as verifyFirstComePolicyInternal, verifyRecoveryEpochUpdate as verifyRecoveryEpochUpdateInternal, verifyRosterManifestTranscript as verifyRosterManifestTranscriptInternal, verifyTargetFinality as verifyTargetFinalityInternal, } from './internal/election-foundation/index.js';
|
|
2
|
+
import { loadTranscriptCoreKernel } from './kernel.js';
|
|
3
|
+
export const deriveThresholdProfile = (input) => deriveThresholdProfileInternal(input);
|
|
4
|
+
export function validatePollSpec(input) {
|
|
5
|
+
return validatePollSpecFromUnknownInternal(input);
|
|
6
|
+
}
|
|
7
|
+
export const isValidLifecycleTransition = (transition) => isValidLifecycleTransitionInternal(transition);
|
|
8
|
+
export const deriveLifecycleLabels = (input) => deriveLifecycleLabelsInternal(input);
|
|
9
|
+
export const evaluateActionCapability = (action, context) => evaluateActionCapabilityInternal(action, context);
|
|
10
|
+
export const verifyBoardConsistency = (input) => verifyBoardConsistencyInternal(input);
|
|
11
|
+
export const verifyCastReceiptShell = (input) => verifyCastReceiptShellInternal(input);
|
|
12
|
+
export const verifyCloseRecordShell = (input) => verifyCloseRecordShellInternal(input);
|
|
13
|
+
export const verifyTargetFinality = (input) => verifyTargetFinalityInternal(input);
|
|
14
|
+
export const deriveValidatedFirstComeOrder = (input) => deriveValidatedFirstComeOrderInternal(input);
|
|
15
|
+
export const verifyFirstComePolicy = (input) => verifyFirstComePolicyInternal(input);
|
|
16
|
+
export const verifyRosterManifestTranscript = (input) => verifyRosterManifestTranscriptInternal(input);
|
|
17
|
+
export const isActionCurrentForRecoveryEpoch = (input) => isActionCurrentForRecoveryEpochInternal(input);
|
|
18
|
+
export const verifyRecoveryEpochUpdate = (input) => verifyRecoveryEpochUpdateInternal(input);
|
|
19
|
+
export const verifyTranscriptCoreFixture = async (fixture) => {
|
|
20
|
+
const kernel = await loadTranscriptCoreKernel();
|
|
21
|
+
const verification = kernel.verifyFixture(fixture);
|
|
22
|
+
if ('expectedErrorCode' in verification) {
|
|
23
|
+
return {
|
|
24
|
+
caseName: verification.caseName,
|
|
25
|
+
label: 'TranscriptCoreRejected',
|
|
26
|
+
statusLabels: [],
|
|
27
|
+
rejection: {
|
|
28
|
+
code: verification.expectedErrorCode,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
caseName: verification.caseName,
|
|
34
|
+
label: 'TranscriptCoreVerified',
|
|
35
|
+
objectHash512: verification.objectHash512,
|
|
36
|
+
chunkRoot: verification.chunkRoot,
|
|
37
|
+
statusLabels: verification.statusLabels,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { shake256 } from '@noble/hashes/sha3.js';
|
|
2
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
3
|
+
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
|
|
4
|
+
const textEncoder = new TextEncoder();
|
|
5
|
+
const hash512PreimagePrefix = textEncoder.encode('sealed.vote/v1/hash512');
|
|
6
|
+
const mlDsaContextByteLimit = 255;
|
|
7
|
+
const supportedMlDsaContextString = 'sealed-lattice:v1';
|
|
8
|
+
const mlDsa65PublicKeyByteLength = ml_dsa65.lengths.publicKey;
|
|
9
|
+
const mlDsa65SecretKeyByteLength = ml_dsa65.lengths.secretKey;
|
|
10
|
+
const mlDsa65SignatureByteLength = ml_dsa65.lengths.signature;
|
|
11
|
+
export const protocolDigestNamespaceValues = [
|
|
12
|
+
'BoardEntryDigest',
|
|
13
|
+
'BoardRootDigest',
|
|
14
|
+
'BoardPolicyDigest',
|
|
15
|
+
'PollSpecDigest',
|
|
16
|
+
'PublicKeyDigest',
|
|
17
|
+
'RegistrationEntryDigest',
|
|
18
|
+
'ReceiverKeyRegistrationDigest',
|
|
19
|
+
'TrusteeSetupEntryDigest',
|
|
20
|
+
'ElectionManifestDigest',
|
|
21
|
+
'RosterDigest',
|
|
22
|
+
'BoardHeadDigest',
|
|
23
|
+
'RecoveryEpochUpdateDigest',
|
|
24
|
+
'ActionContextDigest',
|
|
25
|
+
'BallotPackageDigest',
|
|
26
|
+
'BallotSetDigest',
|
|
27
|
+
'CastReceiptDigest',
|
|
28
|
+
'CloseRecordDigest',
|
|
29
|
+
'WitnessCheckpointDigest',
|
|
30
|
+
'ConflictingHeadEvidenceDigest',
|
|
31
|
+
'InclusionProofDigest',
|
|
32
|
+
'FirstComeOrderDigest',
|
|
33
|
+
'DuplicateBallotPolicyDigest',
|
|
34
|
+
'FirstComePolicyDigest',
|
|
35
|
+
'TargetFinalityPolicyDigest',
|
|
36
|
+
'WitnessPolicyDigest',
|
|
37
|
+
'RecoveryPolicyDigest',
|
|
38
|
+
'SignedRootDigest',
|
|
39
|
+
'ProtocolSignatureEnvelopeDigest',
|
|
40
|
+
'ProviderBuildDigest',
|
|
41
|
+
'ThresholdProfileDigest',
|
|
42
|
+
'HEParamDigest',
|
|
43
|
+
'CiphertextRoot',
|
|
44
|
+
'PlaintextRoot',
|
|
45
|
+
'EvalKeyRoot',
|
|
46
|
+
'TopKCircuitDigest',
|
|
47
|
+
'RotSetDigest',
|
|
48
|
+
'TargetLayoutDigest',
|
|
49
|
+
'PublicSlotMaskDigest',
|
|
50
|
+
'AggregateDerivationComponentDigest',
|
|
51
|
+
'AggregateContributionDigest',
|
|
52
|
+
'AggregateReadyRecordDigest',
|
|
53
|
+
'AggregateSelectionPolicyDigest',
|
|
54
|
+
'PostVotingClosedContextDigest',
|
|
55
|
+
'EvaluationContextDigest',
|
|
56
|
+
'TopKEvaluationRecordDigest',
|
|
57
|
+
'TargetFinalityRecordDigest',
|
|
58
|
+
'EvaluationReplayAttestationDigest',
|
|
59
|
+
'TargetAcceptedRecordDigest',
|
|
60
|
+
'TargetPreimageDigest',
|
|
61
|
+
'TopKDecryptionShareDigest',
|
|
62
|
+
'VerifiedTopKResultDigest',
|
|
63
|
+
'EvaluationProofRoot',
|
|
64
|
+
'CPADProfileDigest',
|
|
65
|
+
'ThresholdDecryptionProfileDigest',
|
|
66
|
+
'BridgeProofRecordDigest',
|
|
67
|
+
'BridgeProofProfileId',
|
|
68
|
+
'ProofPrimeParamDigest',
|
|
69
|
+
'ProofPrimeCiphertextRoot',
|
|
70
|
+
'ProofPrimePublicKeyRoot',
|
|
71
|
+
'ProofPrimeToQDataKeyConsistencyDigest',
|
|
72
|
+
'DerivedAggregateCiphertextRoot',
|
|
73
|
+
'CanonicalCiphertextConventionDigest',
|
|
74
|
+
'BFVBatchEncoderDigest',
|
|
75
|
+
'BridgeLayoutDigest',
|
|
76
|
+
'AggregateShareCommitmentDigest',
|
|
77
|
+
'ShareCommitmentDigest',
|
|
78
|
+
'BrakerskiProfileDigest',
|
|
79
|
+
'BrakerskiDeltaDigest',
|
|
80
|
+
'BrakerskiShareVerificationKeyRoot',
|
|
81
|
+
'TargetDecryptionPreparationRecordDigest',
|
|
82
|
+
'BrakerskiPreprocessRecordDigest',
|
|
83
|
+
'BrakerskiPreprocessTokenDigest',
|
|
84
|
+
'BrakerskiPreprocessUseRecordDigest',
|
|
85
|
+
'QTargetDigest',
|
|
86
|
+
'MobileProfileCertDigest',
|
|
87
|
+
'BridgeMobileCertDigest',
|
|
88
|
+
'BridgeBatchingCertDigest',
|
|
89
|
+
'AggregateBridgeProverCertDigest',
|
|
90
|
+
'EncryptedEnvelopeRoot',
|
|
91
|
+
];
|
|
92
|
+
const emptySignatureVerificationResult = (code, message, objectDigest) => ({
|
|
93
|
+
ok: false,
|
|
94
|
+
statusLabels: [],
|
|
95
|
+
acceptedDigests: [],
|
|
96
|
+
refusedObjects: [
|
|
97
|
+
{
|
|
98
|
+
code,
|
|
99
|
+
message,
|
|
100
|
+
objectDigest,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
const successfulSignatureVerification = (signatureDigest) => ({
|
|
105
|
+
ok: true,
|
|
106
|
+
statusLabels: [],
|
|
107
|
+
acceptedDigests: [signatureDigest],
|
|
108
|
+
refusedObjects: [],
|
|
109
|
+
});
|
|
110
|
+
const isNonNegativeInteger = (value) => Number.isInteger(value) && value >= 0;
|
|
111
|
+
const isLowercaseHex = (value) => /^[0-9a-f]*$/u.test(value) && value.length % 2 === 0;
|
|
112
|
+
const isPlainObject = (value) => typeof value === 'object' &&
|
|
113
|
+
value !== null &&
|
|
114
|
+
!Array.isArray(value) &&
|
|
115
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
116
|
+
const normalizeHex = (value) => value.toLowerCase();
|
|
117
|
+
const normalizeCanonicalValue = (value) => {
|
|
118
|
+
if (value === null) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === 'number') {
|
|
125
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
126
|
+
throw new TypeError('Canonical numeric fields must be integers.');
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
return value.map((entry) => normalizeCanonicalValue(entry));
|
|
132
|
+
}
|
|
133
|
+
if (isPlainObject(value)) {
|
|
134
|
+
const normalized = {};
|
|
135
|
+
for (const key of Object.keys(value).sort()) {
|
|
136
|
+
const entry = value[key];
|
|
137
|
+
if (entry === undefined) {
|
|
138
|
+
throw new TypeError('Canonical objects cannot contain undefined.');
|
|
139
|
+
}
|
|
140
|
+
normalized[key] = normalizeCanonicalValue(entry);
|
|
141
|
+
}
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
throw new TypeError('Unsupported canonical value.');
|
|
145
|
+
};
|
|
146
|
+
export const canonicalJson = (value) => JSON.stringify(normalizeCanonicalValue(value));
|
|
147
|
+
const appendVarUint = (output, value) => {
|
|
148
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
149
|
+
throw new TypeError('Varuint values must be non-negative safe integers.');
|
|
150
|
+
}
|
|
151
|
+
let remainingValue = value;
|
|
152
|
+
for (;;) {
|
|
153
|
+
let byte = remainingValue & 0x7f;
|
|
154
|
+
remainingValue = Math.floor(remainingValue / 128);
|
|
155
|
+
if (remainingValue !== 0) {
|
|
156
|
+
byte |= 0x80;
|
|
157
|
+
}
|
|
158
|
+
output.push(byte);
|
|
159
|
+
if (remainingValue === 0) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const appendBytes = (output, value) => {
|
|
165
|
+
appendVarUint(output, value.byteLength);
|
|
166
|
+
output.push(...value);
|
|
167
|
+
};
|
|
168
|
+
export const hash512 = (domain, parts) => {
|
|
169
|
+
const preimage = Array.from(hash512PreimagePrefix);
|
|
170
|
+
appendBytes(preimage, textEncoder.encode(domain));
|
|
171
|
+
appendVarUint(preimage, parts.length);
|
|
172
|
+
for (const part of parts) {
|
|
173
|
+
appendBytes(preimage, part);
|
|
174
|
+
}
|
|
175
|
+
return shake256(Uint8Array.from(preimage), { dkLen: 64 });
|
|
176
|
+
};
|
|
177
|
+
export const hash512Hex = (domain, parts) => bytesToHex(hash512(domain, parts));
|
|
178
|
+
const pascalCaseToKebabCase = (value) => value
|
|
179
|
+
.replace(/([A-Z]+)([A-Z][a-z])/gu, '$1-$2')
|
|
180
|
+
.replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
|
|
181
|
+
.toLowerCase();
|
|
182
|
+
export const resolveProtocolDigestDomain = (namespace) => {
|
|
183
|
+
if (namespace.startsWith('sealed-lattice-root/')) {
|
|
184
|
+
return namespace;
|
|
185
|
+
}
|
|
186
|
+
if (!/^[A-Z][A-Za-z0-9]*$/u.test(namespace)) {
|
|
187
|
+
throw new TypeError('Protocol digest namespace must be a reserved PascalCase name or an explicit sealed-lattice-root domain.');
|
|
188
|
+
}
|
|
189
|
+
return `sealed-lattice-root/${pascalCaseToKebabCase(namespace)}-v1`;
|
|
190
|
+
};
|
|
191
|
+
export const deriveProtocolDigest = (namespace, value) => hash512Hex(resolveProtocolDigestDomain(namespace), [
|
|
192
|
+
textEncoder.encode(canonicalJson(value)),
|
|
193
|
+
]);
|
|
194
|
+
export const derivePolicyDigest = (namespace, policy) => deriveProtocolDigest(namespace, policy);
|
|
195
|
+
const canonicalSignedRootMessage = (signedRoot) => textEncoder.encode(canonicalJson(signedRoot));
|
|
196
|
+
const decodeHexField = (value, expectedByteLength, fieldName) => {
|
|
197
|
+
if (!isLowercaseHex(value)) {
|
|
198
|
+
throw new Error(`${fieldName} must be lowercase canonical hex.`);
|
|
199
|
+
}
|
|
200
|
+
const bytes = hexToBytes(value);
|
|
201
|
+
if (bytes.byteLength !== expectedByteLength) {
|
|
202
|
+
throw new Error(`${fieldName} must be ${String(expectedByteLength)} bytes.`);
|
|
203
|
+
}
|
|
204
|
+
return bytes;
|
|
205
|
+
};
|
|
206
|
+
export const deriveMlDsaContextByteLength = (contextString) => textEncoder.encode(contextString).byteLength;
|
|
207
|
+
export const createMlDsaSignatureProfileFixture = (overrides = {}) => {
|
|
208
|
+
const contextString = overrides.contextString ?? 'sealed-lattice:v1';
|
|
209
|
+
const contextStringByteLength = overrides.contextStringByteLength ??
|
|
210
|
+
deriveMlDsaContextByteLength(contextString);
|
|
211
|
+
return {
|
|
212
|
+
algorithm: 'ML-DSA-65',
|
|
213
|
+
mode: overrides.mode ?? 'PureMLDSA',
|
|
214
|
+
providerName: overrides.providerName ?? 'deterministic-fixture',
|
|
215
|
+
providerVersion: overrides.providerVersion ?? '1',
|
|
216
|
+
providerBuildHash: overrides.providerBuildHash ??
|
|
217
|
+
deriveProtocolDigest('ProviderBuildDigest', {
|
|
218
|
+
providerName: 'deterministic-fixture',
|
|
219
|
+
providerVersion: '1',
|
|
220
|
+
}),
|
|
221
|
+
fips204Version: overrides.fips204Version ?? 'FIPS 204',
|
|
222
|
+
errataStatus: overrides.errataStatus ?? 'none',
|
|
223
|
+
contextString,
|
|
224
|
+
contextStringByteLength,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
export const deriveMlDsaPublicKeyDigest = (publicKeyBytesHex) => deriveProtocolDigest('PublicKeyDigest', {
|
|
228
|
+
algorithm: 'ML-DSA-65',
|
|
229
|
+
publicKeyBytesHex: normalizeHex(publicKeyBytesHex),
|
|
230
|
+
});
|
|
231
|
+
export const createMlDsaKeyPairFixture = (seedLabel) => {
|
|
232
|
+
const seed = deriveProtocolDigest('ProviderBuildDigest', {
|
|
233
|
+
purpose: 'ml-dsa-fixture-seed',
|
|
234
|
+
seedLabel,
|
|
235
|
+
}).slice(0, 64);
|
|
236
|
+
const keyPair = ml_dsa65.keygen(hexToBytes(seed));
|
|
237
|
+
const publicKeyBytesHex = bytesToHex(keyPair.publicKey);
|
|
238
|
+
return {
|
|
239
|
+
publicKeyBytesHex,
|
|
240
|
+
publicKeyDigest: deriveMlDsaPublicKeyDigest(publicKeyBytesHex),
|
|
241
|
+
secretKeyBytesHex: bytesToHex(keyPair.secretKey),
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
export const deriveCanonicalSignedRootDigest = (signedRoot) => deriveProtocolDigest('SignedRootDigest', signedRoot);
|
|
245
|
+
export const deriveProtocolSignatureDigest = (signature) => deriveProtocolDigest('ProtocolSignatureEnvelopeDigest', {
|
|
246
|
+
profile: signature.profile,
|
|
247
|
+
publicKeyBytesHex: normalizeHex(signature.publicKeyBytesHex),
|
|
248
|
+
publicKeyDigest: signature.publicKeyDigest,
|
|
249
|
+
signatureBytesHex: normalizeHex(signature.signatureBytesHex),
|
|
250
|
+
signedRoot: signature.signedRoot,
|
|
251
|
+
});
|
|
252
|
+
export const createProtocolSignatureFixture = (input) => {
|
|
253
|
+
const secretKey = decodeHexField(normalizeHex(input.secretKeyBytesHex), mlDsa65SecretKeyByteLength, 'secretKeyBytesHex');
|
|
254
|
+
const message = canonicalSignedRootMessage(input.signedRoot);
|
|
255
|
+
const signatureBytes = ml_dsa65.sign(message, secretKey, {
|
|
256
|
+
context: textEncoder.encode(input.profile.contextString),
|
|
257
|
+
extraEntropy: false,
|
|
258
|
+
});
|
|
259
|
+
const signature = {
|
|
260
|
+
profile: input.profile,
|
|
261
|
+
publicKeyBytesHex: normalizeHex(input.publicKeyBytesHex),
|
|
262
|
+
publicKeyDigest: input.publicKeyDigest,
|
|
263
|
+
signatureBytesHex: bytesToHex(signatureBytes),
|
|
264
|
+
signedRoot: input.signedRoot,
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
...signature,
|
|
268
|
+
signatureDigest: deriveProtocolSignatureDigest(signature),
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
const validateProfile = (signature) => {
|
|
272
|
+
const byteLength = deriveMlDsaContextByteLength(signature.profile.contextString);
|
|
273
|
+
if (signature.profile.algorithm !== 'ML-DSA-65') {
|
|
274
|
+
return emptySignatureVerificationResult('InvalidSignature', 'Signature profile must use ML-DSA-65.', signature.signatureDigest);
|
|
275
|
+
}
|
|
276
|
+
if (signature.profile.mode !== 'PureMLDSA') {
|
|
277
|
+
return emptySignatureVerificationResult('InvalidSignature', 'Only PureMLDSA signatures are supported by this verifier.', signature.signatureDigest);
|
|
278
|
+
}
|
|
279
|
+
if (signature.profile.providerName.length === 0 ||
|
|
280
|
+
signature.profile.providerVersion.length === 0 ||
|
|
281
|
+
signature.profile.providerBuildHash.length === 0 ||
|
|
282
|
+
signature.profile.fips204Version.length === 0 ||
|
|
283
|
+
signature.profile.errataStatus.length === 0) {
|
|
284
|
+
return emptySignatureVerificationResult('InvalidSignature', 'Signature profile metadata must be fully bound.', signature.signatureDigest);
|
|
285
|
+
}
|
|
286
|
+
if (byteLength > mlDsaContextByteLimit) {
|
|
287
|
+
return emptySignatureVerificationResult('InvalidMlDsaContext', 'ML-DSA context strings must be at most 255 bytes.', signature.signatureDigest);
|
|
288
|
+
}
|
|
289
|
+
if (signature.profile.contextStringByteLength !== byteLength) {
|
|
290
|
+
return emptySignatureVerificationResult('InvalidMlDsaContext', 'ML-DSA context string byte length does not match the profile.', signature.signatureDigest);
|
|
291
|
+
}
|
|
292
|
+
if (signature.profile.contextString !== supportedMlDsaContextString) {
|
|
293
|
+
return emptySignatureVerificationResult('InvalidMlDsaContext', 'ML-DSA context string does not match the supported protocol context.', signature.signatureDigest);
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
};
|
|
297
|
+
const validateSignatureMaterial = (signature) => {
|
|
298
|
+
try {
|
|
299
|
+
decodeHexField(normalizeHex(signature.publicKeyBytesHex), mlDsa65PublicKeyByteLength, 'publicKeyBytesHex');
|
|
300
|
+
decodeHexField(normalizeHex(signature.signatureBytesHex), mlDsa65SignatureByteLength, 'signatureBytesHex');
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return emptySignatureVerificationResult('InvalidSignature', 'Signature envelope contains malformed ML-DSA key or signature bytes.', signature.signatureDigest);
|
|
304
|
+
}
|
|
305
|
+
const expectedPublicKeyDigest = deriveMlDsaPublicKeyDigest(normalizeHex(signature.publicKeyBytesHex));
|
|
306
|
+
if (signature.publicKeyDigest !== expectedPublicKeyDigest) {
|
|
307
|
+
return emptySignatureVerificationResult('WrongPublicKey', 'Signature public key digest does not match the ML-DSA public key bytes.', signature.signatureDigest);
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
};
|
|
311
|
+
const validateSignedRootShape = (signedRoot) => {
|
|
312
|
+
const signedRootRecord = signedRoot;
|
|
313
|
+
const requiredFields = [
|
|
314
|
+
'objectType',
|
|
315
|
+
'objectVersion',
|
|
316
|
+
'ceremonyId',
|
|
317
|
+
'manifestHash',
|
|
318
|
+
'boardHeadHash',
|
|
319
|
+
'objectRoot',
|
|
320
|
+
'chunkMerkleRoot',
|
|
321
|
+
'byteLength',
|
|
322
|
+
'signerRole',
|
|
323
|
+
'signerIdentity',
|
|
324
|
+
'recoveryEpoch',
|
|
325
|
+
'deviceEpoch',
|
|
326
|
+
'contextDigest',
|
|
327
|
+
];
|
|
328
|
+
const missingField = requiredFields.find((fieldName) => !Object.prototype.hasOwnProperty.call(signedRootRecord, fieldName));
|
|
329
|
+
if (missingField !== undefined) {
|
|
330
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', `Signed roots must bind ${missingField}.`);
|
|
331
|
+
}
|
|
332
|
+
const objectRootPresent = typeof signedRoot.objectRoot === 'string';
|
|
333
|
+
const chunkMerkleRootPresent = typeof signedRoot.chunkMerkleRoot === 'string';
|
|
334
|
+
if (!objectRootPresent && !chunkMerkleRootPresent) {
|
|
335
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signed roots must bind an object root or chunk Merkle root.');
|
|
336
|
+
}
|
|
337
|
+
if (objectRootPresent && chunkMerkleRootPresent) {
|
|
338
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signed roots must bind exactly one object root or chunk Merkle root.');
|
|
339
|
+
}
|
|
340
|
+
if ((signedRoot.objectRoot !== null && !objectRootPresent) ||
|
|
341
|
+
(signedRoot.chunkMerkleRoot !== null && !chunkMerkleRootPresent) ||
|
|
342
|
+
(signedRoot.manifestHash !== null &&
|
|
343
|
+
typeof signedRoot.manifestHash !== 'string') ||
|
|
344
|
+
(signedRoot.boardHeadHash !== null &&
|
|
345
|
+
typeof signedRoot.boardHeadHash !== 'string')) {
|
|
346
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signed-root digest bindings must be canonical digest strings or null.');
|
|
347
|
+
}
|
|
348
|
+
if (!isNonNegativeInteger(signedRoot.objectVersion) ||
|
|
349
|
+
!isNonNegativeInteger(signedRoot.byteLength) ||
|
|
350
|
+
!isNonNegativeInteger(signedRoot.recoveryEpoch) ||
|
|
351
|
+
!isNonNegativeInteger(signedRoot.deviceEpoch)) {
|
|
352
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signed root version, byte length, and epochs must be non-negative integers.');
|
|
353
|
+
}
|
|
354
|
+
if (signedRoot.ceremonyId.length === 0 ||
|
|
355
|
+
signedRoot.signerIdentity.length === 0 ||
|
|
356
|
+
signedRoot.contextDigest.length === 0) {
|
|
357
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signed roots must bind ceremony, signer identity, and context digest.');
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
};
|
|
361
|
+
const validateExpectation = (signature, expectation) => {
|
|
362
|
+
const { signedRoot } = signature;
|
|
363
|
+
if (expectation.objectType !== undefined &&
|
|
364
|
+
signedRoot.objectType !== expectation.objectType) {
|
|
365
|
+
return emptySignatureVerificationResult('WrongObjectType', 'Signature root object type does not match the expected object.', signature.signatureDigest);
|
|
366
|
+
}
|
|
367
|
+
if (expectation.objectVersion !== undefined &&
|
|
368
|
+
signedRoot.objectVersion !== expectation.objectVersion) {
|
|
369
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signature root object version does not match the expected version.', signature.signatureDigest);
|
|
370
|
+
}
|
|
371
|
+
if (expectation.signerRole !== undefined &&
|
|
372
|
+
signedRoot.signerRole !== expectation.signerRole) {
|
|
373
|
+
return emptySignatureVerificationResult('WrongSignerRole', 'Signature root signer role does not match the expected role.', signature.signatureDigest);
|
|
374
|
+
}
|
|
375
|
+
if (expectation.signerIdentity !== undefined &&
|
|
376
|
+
signedRoot.signerIdentity !== expectation.signerIdentity) {
|
|
377
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signature root signer identity does not match the expected identity.', signature.signatureDigest);
|
|
378
|
+
}
|
|
379
|
+
if (expectation.ceremonyId !== undefined &&
|
|
380
|
+
signedRoot.ceremonyId !== expectation.ceremonyId) {
|
|
381
|
+
return emptySignatureVerificationResult('WrongCeremony', 'Signature root ceremony does not match the expected ceremony.', signature.signatureDigest);
|
|
382
|
+
}
|
|
383
|
+
if (expectation.publicKeyDigest !== undefined &&
|
|
384
|
+
signature.publicKeyDigest !== expectation.publicKeyDigest) {
|
|
385
|
+
return emptySignatureVerificationResult('WrongPublicKey', 'Signature public key digest does not match the expected key.', signature.signatureDigest);
|
|
386
|
+
}
|
|
387
|
+
if (expectation.manifestHash !== undefined &&
|
|
388
|
+
signedRoot.manifestHash !== expectation.manifestHash) {
|
|
389
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signature root manifest digest does not match the expected manifest.', signature.signatureDigest);
|
|
390
|
+
}
|
|
391
|
+
if (expectation.objectRoot !== undefined &&
|
|
392
|
+
signedRoot.objectRoot !== expectation.objectRoot) {
|
|
393
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signature root object digest does not match the signed object.', signature.signatureDigest);
|
|
394
|
+
}
|
|
395
|
+
if (expectation.boardHeadHash !== undefined &&
|
|
396
|
+
signedRoot.boardHeadHash !== expectation.boardHeadHash) {
|
|
397
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signature root board-head digest does not match the expected head.', signature.signatureDigest);
|
|
398
|
+
}
|
|
399
|
+
if (expectation.contextDigest !== undefined &&
|
|
400
|
+
signedRoot.contextDigest !== expectation.contextDigest) {
|
|
401
|
+
return emptySignatureVerificationResult('InvalidSignedRoot', 'Signature root context digest does not match the expected context.', signature.signatureDigest);
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
};
|
|
405
|
+
const verifySignedObjectSignatureInner = (signature, expectation = {}) => {
|
|
406
|
+
const profileFailure = validateProfile(signature);
|
|
407
|
+
if (profileFailure !== undefined) {
|
|
408
|
+
return profileFailure;
|
|
409
|
+
}
|
|
410
|
+
const materialFailure = validateSignatureMaterial(signature);
|
|
411
|
+
if (materialFailure !== undefined) {
|
|
412
|
+
return materialFailure;
|
|
413
|
+
}
|
|
414
|
+
const shapeFailure = validateSignedRootShape(signature.signedRoot);
|
|
415
|
+
if (shapeFailure !== undefined) {
|
|
416
|
+
return shapeFailure;
|
|
417
|
+
}
|
|
418
|
+
const expectationFailure = validateExpectation(signature, expectation);
|
|
419
|
+
if (expectationFailure !== undefined) {
|
|
420
|
+
return expectationFailure;
|
|
421
|
+
}
|
|
422
|
+
const expectedSignatureDigest = deriveProtocolSignatureDigest({
|
|
423
|
+
profile: signature.profile,
|
|
424
|
+
publicKeyBytesHex: normalizeHex(signature.publicKeyBytesHex),
|
|
425
|
+
publicKeyDigest: signature.publicKeyDigest,
|
|
426
|
+
signatureBytesHex: normalizeHex(signature.signatureBytesHex),
|
|
427
|
+
signedRoot: signature.signedRoot,
|
|
428
|
+
});
|
|
429
|
+
if (signature.signatureDigest !== expectedSignatureDigest) {
|
|
430
|
+
return emptySignatureVerificationResult('InvalidSignature', 'Signature digest does not verify for the canonical signed root.', signature.signatureDigest);
|
|
431
|
+
}
|
|
432
|
+
const publicKeyBytes = decodeHexField(normalizeHex(signature.publicKeyBytesHex), mlDsa65PublicKeyByteLength, 'publicKeyBytesHex');
|
|
433
|
+
const signatureBytes = decodeHexField(normalizeHex(signature.signatureBytesHex), mlDsa65SignatureByteLength, 'signatureBytesHex');
|
|
434
|
+
const signatureValid = ml_dsa65.verify(signatureBytes, canonicalSignedRootMessage(signature.signedRoot), publicKeyBytes, {
|
|
435
|
+
context: textEncoder.encode(signature.profile.contextString),
|
|
436
|
+
});
|
|
437
|
+
if (!signatureValid) {
|
|
438
|
+
return emptySignatureVerificationResult('InvalidSignature', 'ML-DSA signature does not verify for the canonical signed root.', signature.signatureDigest);
|
|
439
|
+
}
|
|
440
|
+
return successfulSignatureVerification(signature.signatureDigest);
|
|
441
|
+
};
|
|
442
|
+
export const verifySignedObjectSignature = (signature, expectation = {}) => {
|
|
443
|
+
try {
|
|
444
|
+
return verifySignedObjectSignatureInner(signature, expectation);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return emptySignatureVerificationResult('InvalidSignature', 'Signature envelope is not a canonical ML-DSA signed-root envelope.', signature
|
|
448
|
+
?.signatureDigest);
|
|
449
|
+
}
|
|
450
|
+
};
|