@agimon-ai/po-web-security 0.0.0-alpha.52 → 0.0.0-alpha.54
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 +20 -0
- package/dist/browser.cjs +1 -1
- package/dist/browser.d.mts +2 -1
- package/dist/browser.mjs +1 -1
- package/dist/deviceProof.cjs +1 -0
- package/dist/deviceProof.d.mts +7 -0
- package/dist/deviceProof.mjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.d.mts +3 -2
- package/dist/node.mjs +1 -1
- package/dist/services/deviceSignature/index.cjs +1 -1
- package/dist/services/deviceSignature/index.cjs.map +1 -1
- package/dist/services/deviceSignature/index.mjs +1 -1
- package/dist/services/deviceSignature/index.mjs.map +1 -1
- package/dist/services/federationEnvelope/index.cjs +2 -0
- package/dist/services/federationEnvelope/index.cjs.map +1 -0
- package/dist/services/federationEnvelope/index.d.mts +13 -0
- package/dist/services/federationEnvelope/index.d.mts.map +1 -0
- package/dist/services/federationEnvelope/index.mjs +2 -0
- package/dist/services/federationEnvelope/index.mjs.map +1 -0
- package/dist/services/federationIssuerHttp/index.cjs +2 -0
- package/dist/services/federationIssuerHttp/index.cjs.map +1 -0
- package/dist/services/federationIssuerHttp/index.d.mts +13 -0
- package/dist/services/federationIssuerHttp/index.d.mts.map +1 -0
- package/dist/services/federationIssuerHttp/index.mjs +2 -0
- package/dist/services/federationIssuerHttp/index.mjs.map +1 -0
- package/dist/services/federationIssuerHttp/type.d.mts +46 -0
- package/dist/services/federationIssuerHttp/type.d.mts.map +1 -0
- package/dist/services/federationVerifier/index.cjs +2 -0
- package/dist/services/federationVerifier/index.cjs.map +1 -0
- package/dist/services/federationVerifier/index.d.mts +12 -0
- package/dist/services/federationVerifier/index.d.mts.map +1 -0
- package/dist/services/federationVerifier/index.mjs +2 -0
- package/dist/services/federationVerifier/index.mjs.map +1 -0
- package/dist/services/federationVerifier/type.d.mts +129 -0
- package/dist/services/federationVerifier/type.d.mts.map +1 -0
- package/dist/types/federation.cjs +2 -0
- package/dist/types/federation.cjs.map +1 -0
- package/dist/types/federation.d.mts +40 -0
- package/dist/types/federation.d.mts.map +1 -0
- package/dist/types/federation.mjs +2 -0
- package/dist/types/federation.mjs.map +1 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -85,6 +85,26 @@ import { sealedTransport } from '@agimon-ai/po-web-security/browser';
|
|
|
85
85
|
import { createBundleSigner, createHostHandshake } from '@agimon-ai/po-web-security/node';
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## Federation destination verification
|
|
89
|
+
|
|
90
|
+
`deviceProof` exports `createFederationVerifier`, `FederationReplayStore`, and
|
|
91
|
+
`createFederationIssuerHttp`. The verifier checks ES256 issuer JWTs, C3 device proofs,
|
|
92
|
+
C4 host proofs and C5 grants, exact recovery epochs, request scope/body binding and
|
|
93
|
+
atomic durable nonce admission. C1 binds the device key thumbprint; C2 binds the
|
|
94
|
+
calling host key thumbprint. Token org/role authority lasts only until its conservative
|
|
95
|
+
expiry. It does not create membership or authorize a workspace by itself.
|
|
96
|
+
|
|
97
|
+
Issuer origins and initial JWKS must be locally pinned at claim/join. Unknown `kid`
|
|
98
|
+
rotation and device-key/feed HTTP requests use only that origin with redirects refused.
|
|
99
|
+
The HTTP adapter persists cursor/feed state through injected ports; missing state or
|
|
100
|
+
state older than ten minutes fails closed when the issuer is unavailable. Device keys
|
|
101
|
+
come from a trusted local registry or the host-authenticated issuer lookup, not envelopes.
|
|
102
|
+
The destination supplies its home-host ACL/provider-consent authorization callback.
|
|
103
|
+
|
|
104
|
+
All replay times and envelope dates are milliseconds; JWT NumericDate claims are seconds.
|
|
105
|
+
Browser/native signers use `federationEnvelopeFields` from `browser` with
|
|
106
|
+
`attestationBytes(tag, fields)`. `browser` also exports `FEDERATION_FRESHNESS`.
|
|
107
|
+
|
|
88
108
|
## Development
|
|
89
109
|
|
|
90
110
|
```bash
|
package/dist/browser.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./services/browserBundleVerifier/index.cjs"),n=require("./services/browserSealedChannel/index.cjs"),r=require("./services/sealedTransport/index.cjs");exports.APP_ASSET_MAX_BYTES=e.APP_ASSET_MAX_BYTES,exports.APP_DESCRIPTOR_PATH=e.APP_DESCRIPTOR_PATH,exports.APP_PART_PREFIX=e.APP_PART_PREFIX,exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.NATIVE_COMPOSITION_MAX_BYTES=e.NATIVE_COMPOSITION_MAX_BYTES,exports.NATIVE_ENTRY_CONTENT_TYPE=e.NATIVE_ENTRY_CONTENT_TYPE,exports.NATIVE_ENTRY_PATH=e.NATIVE_ENTRY_PATH,exports.WEB_ENTRY_PATH=e.WEB_ENTRY_PATH,exports.assetFor=e.assetFor,exports.attestationBytes=e.attestationBytes,exports.bindBrowserTransport=r.bindBrowserTransport,exports.browserTransport=r.browserTransport,exports.browserTransportForHost=r.browserTransportForHost,exports.canonicalManifest=e.canonicalManifest,exports.channelFromSecret=n.channelFromSecret,exports.connectSealedChannel=n.connectSealedChannel,exports.createSealedTransport=r.createSealedTransport,exports.digestFor=e.digestFor,exports.isBundleManifest=e.isBundleManifest,exports.isSignedBundleManifest=e.isSignedBundleManifest,exports.verifyAttestation=t.verifyAttestation,exports.verifyBundleAsset=t.verifyBundleAsset,exports.verifySignedBundleManifest=t.verifySignedBundleManifest;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./services/browserBundleVerifier/index.cjs"),n=require("./services/browserSealedChannel/index.cjs"),r=require("./services/sealedTransport/index.cjs"),i=require("./types/federation.cjs");exports.APP_ASSET_MAX_BYTES=e.APP_ASSET_MAX_BYTES,exports.APP_DESCRIPTOR_PATH=e.APP_DESCRIPTOR_PATH,exports.APP_PART_PREFIX=e.APP_PART_PREFIX,exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.FEDERATION_FRESHNESS=i.FEDERATION_FRESHNESS,exports.NATIVE_COMPOSITION_MAX_BYTES=e.NATIVE_COMPOSITION_MAX_BYTES,exports.NATIVE_ENTRY_CONTENT_TYPE=e.NATIVE_ENTRY_CONTENT_TYPE,exports.NATIVE_ENTRY_PATH=e.NATIVE_ENTRY_PATH,exports.WEB_ENTRY_PATH=e.WEB_ENTRY_PATH,exports.assetFor=e.assetFor,exports.attestationBytes=e.attestationBytes,exports.bindBrowserTransport=r.bindBrowserTransport,exports.browserTransport=r.browserTransport,exports.browserTransportForHost=r.browserTransportForHost,exports.canonicalManifest=e.canonicalManifest,exports.channelFromSecret=n.channelFromSecret,exports.connectSealedChannel=n.connectSealedChannel,exports.createSealedTransport=r.createSealedTransport,exports.digestFor=e.digestFor,exports.federationEnvelopeFields=i.federationEnvelopeFields,exports.isBundleManifest=e.isBundleManifest,exports.isSignedBundleManifest=e.isSignedBundleManifest,exports.verifyAttestation=t.verifyAttestation,exports.verifyBundleAsset=t.verifyBundleAsset,exports.verifySignedBundleManifest=t.verifySignedBundleManifest;
|
package/dist/browser.d.mts
CHANGED
|
@@ -2,4 +2,5 @@ import { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_
|
|
|
2
2
|
import { BundleAssetVerificationFailure, BundleAssetVerificationResult, BundleVerificationFailure, BundleVerificationResult, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest } from "./services/browserBundleVerifier/index.mjs";
|
|
3
3
|
import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./services/browserSealedChannel/index.mjs";
|
|
4
4
|
import { SealedTransport, bindBrowserTransport, browserTransport, browserTransportForHost, createSealedTransport } from "./services/sealedTransport/index.mjs";
|
|
5
|
-
|
|
5
|
+
import { FEDERATION_FRESHNESS, federationEnvelopeFields } from "./types/federation.mjs";
|
|
6
|
+
export { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleProfile, type BundleVerificationFailure, type BundleVerificationResult, FEDERATION_FRESHNESS, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, type OpenResult, type SealResult, type SealedChannel, type SealedTransport, type SignedBundleManifest, WEB_ENTRY_PATH, assetFor, attestationBytes, bindBrowserTransport, browserTransport, browserTransportForHost, canonicalManifest, channelFromSecret, connectSealedChannel, createSealedTransport, digestFor, federationEnvelopeFields, isBundleManifest, isSignedBundleManifest, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest };
|
package/dist/browser.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{APP_ASSET_MAX_BYTES as e,APP_DESCRIPTOR_PATH as t,APP_PART_PREFIX as n,ATTESTATION_TAG as r,BUNDLE_MANIFEST_ROUTE as i,BUNDLE_MANIFEST_VERSION as a,NATIVE_COMPOSITION_MAX_BYTES as o,NATIVE_ENTRY_CONTENT_TYPE as s,NATIVE_ENTRY_PATH as c,WEB_ENTRY_PATH as l,assetFor as u,attestationBytes as d,canonicalManifest as f,digestFor as p,isBundleManifest as m,isSignedBundleManifest as h}from"./types/bundleManifest.mjs";import{verifyAttestation as g,verifyBundleAsset as _,verifySignedBundleManifest as v}from"./services/browserBundleVerifier/index.mjs";import{channelFromSecret as y,connectSealedChannel as b}from"./services/browserSealedChannel/index.mjs";import{bindBrowserTransport as x,browserTransport as S,browserTransportForHost as C,createSealedTransport as w}from"./services/sealedTransport/index.mjs";export{e as APP_ASSET_MAX_BYTES,t as APP_DESCRIPTOR_PATH,n as APP_PART_PREFIX,r as ATTESTATION_TAG,i as BUNDLE_MANIFEST_ROUTE,a as BUNDLE_MANIFEST_VERSION,o as NATIVE_COMPOSITION_MAX_BYTES,s as NATIVE_ENTRY_CONTENT_TYPE,c as NATIVE_ENTRY_PATH,l as WEB_ENTRY_PATH,u as assetFor,d as attestationBytes,x as bindBrowserTransport,S as browserTransport,C as browserTransportForHost,f as canonicalManifest,y as channelFromSecret,b as connectSealedChannel,w as createSealedTransport,p as digestFor,m as isBundleManifest,h as isSignedBundleManifest,g as verifyAttestation,_ as verifyBundleAsset,v as verifySignedBundleManifest};
|
|
1
|
+
import{APP_ASSET_MAX_BYTES as e,APP_DESCRIPTOR_PATH as t,APP_PART_PREFIX as n,ATTESTATION_TAG as r,BUNDLE_MANIFEST_ROUTE as i,BUNDLE_MANIFEST_VERSION as a,NATIVE_COMPOSITION_MAX_BYTES as o,NATIVE_ENTRY_CONTENT_TYPE as s,NATIVE_ENTRY_PATH as c,WEB_ENTRY_PATH as l,assetFor as u,attestationBytes as d,canonicalManifest as f,digestFor as p,isBundleManifest as m,isSignedBundleManifest as h}from"./types/bundleManifest.mjs";import{verifyAttestation as g,verifyBundleAsset as _,verifySignedBundleManifest as v}from"./services/browserBundleVerifier/index.mjs";import{channelFromSecret as y,connectSealedChannel as b}from"./services/browserSealedChannel/index.mjs";import{bindBrowserTransport as x,browserTransport as S,browserTransportForHost as C,createSealedTransport as w}from"./services/sealedTransport/index.mjs";import{FEDERATION_FRESHNESS as T,federationEnvelopeFields as E}from"./types/federation.mjs";export{e as APP_ASSET_MAX_BYTES,t as APP_DESCRIPTOR_PATH,n as APP_PART_PREFIX,r as ATTESTATION_TAG,i as BUNDLE_MANIFEST_ROUTE,a as BUNDLE_MANIFEST_VERSION,T as FEDERATION_FRESHNESS,o as NATIVE_COMPOSITION_MAX_BYTES,s as NATIVE_ENTRY_CONTENT_TYPE,c as NATIVE_ENTRY_PATH,l as WEB_ENTRY_PATH,u as assetFor,d as attestationBytes,x as bindBrowserTransport,S as browserTransport,C as browserTransportForHost,f as canonicalManifest,y as channelFromSecret,b as connectSealedChannel,w as createSealedTransport,p as digestFor,E as federationEnvelopeFields,m as isBundleManifest,h as isSignedBundleManifest,g as verifyAttestation,_ as verifyBundleAsset,v as verifySignedBundleManifest};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/deviceSignature/index.cjs"),t=require("./services/federationEnvelope/index.cjs"),n=require("./services/federationVerifier/index.cjs"),r=require("./services/federationIssuerHttp/index.cjs");exports.createFederationIssuerHttp=r.createFederationIssuerHttp,exports.createFederationVerifier=n.createFederationVerifier,exports.federationDigest=n.federationDigest,exports.federationKeyThumbprint=n.federationKeyThumbprint,exports.isFederationIssuerOrigin=n.isFederationIssuerOrigin,exports.signEnvelope=t.signEnvelope,exports.verifyDeviceSignature=e.verifyDeviceSignature,exports.verifyEnvelope=t.verifyEnvelope;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { verifyDeviceSignature } from "./services/deviceSignature/index.mjs";
|
|
2
|
+
import { signEnvelope, verifyEnvelope } from "./services/federationEnvelope/index.mjs";
|
|
3
|
+
import { FederationAuthority, FederationCaller, FederationConsentGrant, FederationIssuerPin, FederationJwk, FederationReplayStore, FederationRequestProof, FederationTokenClaims, FederationVerifierPorts, FederationVerifyInput } from "./services/federationVerifier/type.mjs";
|
|
4
|
+
import { createFederationVerifier, federationDigest, federationKeyThumbprint, isFederationIssuerOrigin } from "./services/federationVerifier/index.mjs";
|
|
5
|
+
import { FederationDeviceKeyProof, FederationDeviceKeyResponse, FederationFeedState, FederationIssuerHttpPorts, FederationRevocation } from "./services/federationIssuerHttp/type.mjs";
|
|
6
|
+
import { createFederationIssuerHttp } from "./services/federationIssuerHttp/index.mjs";
|
|
7
|
+
export { type FederationAuthority, type FederationCaller, type FederationConsentGrant, type FederationDeviceKeyProof, type FederationDeviceKeyResponse, type FederationFeedState, type FederationIssuerHttpPorts, type FederationIssuerPin, type FederationJwk, type FederationReplayStore, type FederationRequestProof, type FederationRevocation, type FederationTokenClaims, type FederationVerifierPorts, type FederationVerifyInput, createFederationIssuerHttp, createFederationVerifier, federationDigest, federationKeyThumbprint, isFederationIssuerOrigin, signEnvelope, verifyDeviceSignature, verifyEnvelope };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{verifyDeviceSignature as e}from"./services/deviceSignature/index.mjs";import{signEnvelope as t,verifyEnvelope as n}from"./services/federationEnvelope/index.mjs";import{createFederationVerifier as r,federationDigest as i,federationKeyThumbprint as a,isFederationIssuerOrigin as o}from"./services/federationVerifier/index.mjs";import{createFederationIssuerHttp as s}from"./services/federationIssuerHttp/index.mjs";export{s as createFederationIssuerHttp,r as createFederationVerifier,i as federationDigest,a as federationKeyThumbprint,o as isFederationIssuerOrigin,t as signEnvelope,e as verifyDeviceSignature,n as verifyEnvelope};
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./types/sealedChannel.cjs"),n=require("./services/serialQueue/index.cjs");exports.APP_ASSET_MAX_BYTES=e.APP_ASSET_MAX_BYTES,exports.APP_DESCRIPTOR_PATH=e.APP_DESCRIPTOR_PATH,exports.APP_PART_PREFIX=e.APP_PART_PREFIX,exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.CLIENT_TO_SERVER_INFO=t.CLIENT_TO_SERVER_INFO,exports.MAX_MESSAGES_PER_KEY=t.MAX_MESSAGES_PER_KEY,exports.NATIVE_COMPOSITION_MAX_BYTES=e.NATIVE_COMPOSITION_MAX_BYTES,exports.NATIVE_ENTRY_CONTENT_TYPE=e.NATIVE_ENTRY_CONTENT_TYPE,exports.NATIVE_ENTRY_PATH=e.NATIVE_ENTRY_PATH,exports.NONCE_BYTES=t.NONCE_BYTES,exports.NONCE_COUNTER_BYTES=t.NONCE_COUNTER_BYTES,exports.NONCE_PREFIX_BYTES=t.NONCE_PREFIX_BYTES,exports.SEALED_BODY_HEADER=t.SEALED_BODY_HEADER,exports.SEALED_CLIENT_KEY_HEADER=t.SEALED_CLIENT_KEY_HEADER,exports.SEALED_KEY_PARAM=t.SEALED_KEY_PARAM,exports.SEALED_VERSION=t.SEALED_VERSION,exports.SERVER_TO_CLIENT_INFO=t.SERVER_TO_CLIENT_INFO,exports.WEB_ENTRY_PATH=e.WEB_ENTRY_PATH,exports.assetFor=e.assetFor,exports.attestationBytes=e.attestationBytes,exports.canonicalManifest=e.canonicalManifest,exports.createSerialQueue=n.createSerialQueue,exports.describeSealedFailure=t.describeSealedFailure,exports.digestFor=e.digestFor,exports.infoFor=t.infoFor,exports.isBundleManifest=e.isBundleManifest,exports.isSealedEnvelope=t.isSealedEnvelope,exports.isSignedBundleManifest=e.isSignedBundleManifest;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./types/sealedChannel.cjs"),n=require("./services/serialQueue/index.cjs"),r=require("./types/federation.cjs");exports.APP_ASSET_MAX_BYTES=e.APP_ASSET_MAX_BYTES,exports.APP_DESCRIPTOR_PATH=e.APP_DESCRIPTOR_PATH,exports.APP_PART_PREFIX=e.APP_PART_PREFIX,exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.CLIENT_TO_SERVER_INFO=t.CLIENT_TO_SERVER_INFO,exports.FEDERATION_FRESHNESS=r.FEDERATION_FRESHNESS,exports.FEDERATION_TAGS=r.FEDERATION_TAGS,exports.MAX_MESSAGES_PER_KEY=t.MAX_MESSAGES_PER_KEY,exports.NATIVE_COMPOSITION_MAX_BYTES=e.NATIVE_COMPOSITION_MAX_BYTES,exports.NATIVE_ENTRY_CONTENT_TYPE=e.NATIVE_ENTRY_CONTENT_TYPE,exports.NATIVE_ENTRY_PATH=e.NATIVE_ENTRY_PATH,exports.NONCE_BYTES=t.NONCE_BYTES,exports.NONCE_COUNTER_BYTES=t.NONCE_COUNTER_BYTES,exports.NONCE_PREFIX_BYTES=t.NONCE_PREFIX_BYTES,exports.SEALED_BODY_HEADER=t.SEALED_BODY_HEADER,exports.SEALED_CLIENT_KEY_HEADER=t.SEALED_CLIENT_KEY_HEADER,exports.SEALED_KEY_PARAM=t.SEALED_KEY_PARAM,exports.SEALED_VERSION=t.SEALED_VERSION,exports.SERVER_TO_CLIENT_INFO=t.SERVER_TO_CLIENT_INFO,exports.WEB_ENTRY_PATH=e.WEB_ENTRY_PATH,exports.assetFor=e.assetFor,exports.attestationBytes=e.attestationBytes,exports.canonicalManifest=e.canonicalManifest,exports.createSerialQueue=n.createSerialQueue,exports.describeSealedFailure=t.describeSealedFailure,exports.digestFor=e.digestFor,exports.federationEnvelopeFields=r.federationEnvelopeFields,exports.infoFor=t.infoFor,exports.isBundleManifest=e.isBundleManifest,exports.isSealedEnvelope=t.isSealedEnvelope,exports.isSignedBundleManifest=e.isSignedBundleManifest;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, BundleProfile, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, SignedBundleManifest, WEB_ENTRY_PATH, assetFor, attestationBytes, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.mjs";
|
|
2
2
|
import { CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, SealedDirection, SealedEnvelope, SealedFailure, describeSealedFailure, infoFor, isSealedEnvelope } from "./types/sealedChannel.mjs";
|
|
3
|
+
import { FEDERATION_FRESHNESS, FEDERATION_TAGS, FederationTag, FederationValidity, SignedFederationEnvelope, federationEnvelopeFields } from "./types/federation.mjs";
|
|
3
4
|
import { SerialQueue, createSerialQueue } from "./services/serialQueue/index.mjs";
|
|
4
|
-
export { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleManifest, type BundleProfile, CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, type SealedDirection, type SealedEnvelope, type SealedFailure, type SerialQueue, type SignedBundleManifest, WEB_ENTRY_PATH, assetFor, attestationBytes, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope, isSignedBundleManifest };
|
|
5
|
+
export { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleManifest, type BundleProfile, CLIENT_TO_SERVER_INFO, FEDERATION_FRESHNESS, FEDERATION_TAGS, type FederationTag, type FederationValidity, MAX_MESSAGES_PER_KEY, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, type SealedDirection, type SealedEnvelope, type SealedFailure, type SerialQueue, type SignedBundleManifest, type SignedFederationEnvelope, WEB_ENTRY_PATH, assetFor, attestationBytes, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, federationEnvelopeFields, infoFor, isBundleManifest, isSealedEnvelope, isSignedBundleManifest };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{APP_ASSET_MAX_BYTES as e,APP_DESCRIPTOR_PATH as t,APP_PART_PREFIX as n,ATTESTATION_TAG as r,BUNDLE_MANIFEST_ROUTE as i,BUNDLE_MANIFEST_VERSION as a,NATIVE_COMPOSITION_MAX_BYTES as o,NATIVE_ENTRY_CONTENT_TYPE as s,NATIVE_ENTRY_PATH as c,WEB_ENTRY_PATH as l,assetFor as u,attestationBytes as d,canonicalManifest as f,digestFor as p,isBundleManifest as m,isSignedBundleManifest as h}from"./types/bundleManifest.mjs";import{CLIENT_TO_SERVER_INFO as g,MAX_MESSAGES_PER_KEY as _,NONCE_BYTES as v,NONCE_COUNTER_BYTES as y,NONCE_PREFIX_BYTES as b,SEALED_BODY_HEADER as x,SEALED_CLIENT_KEY_HEADER as S,SEALED_KEY_PARAM as C,SEALED_VERSION as w,SERVER_TO_CLIENT_INFO as T,describeSealedFailure as E,infoFor as D,isSealedEnvelope as O}from"./types/sealedChannel.mjs";import{createSerialQueue as k}from"./services/serialQueue/index.mjs";export{e as APP_ASSET_MAX_BYTES,t as APP_DESCRIPTOR_PATH,n as APP_PART_PREFIX,r as ATTESTATION_TAG,i as BUNDLE_MANIFEST_ROUTE,a as BUNDLE_MANIFEST_VERSION,g as CLIENT_TO_SERVER_INFO,_ as MAX_MESSAGES_PER_KEY,o as NATIVE_COMPOSITION_MAX_BYTES,s as NATIVE_ENTRY_CONTENT_TYPE,c as NATIVE_ENTRY_PATH,v as NONCE_BYTES,y as NONCE_COUNTER_BYTES,b as NONCE_PREFIX_BYTES,x as SEALED_BODY_HEADER,S as SEALED_CLIENT_KEY_HEADER,C as SEALED_KEY_PARAM,w as SEALED_VERSION,T as SERVER_TO_CLIENT_INFO,l as WEB_ENTRY_PATH,u as assetFor,d as attestationBytes,f as canonicalManifest,k as createSerialQueue,E as describeSealedFailure,p as digestFor,D as infoFor,m as isBundleManifest,O as isSealedEnvelope,h as isSignedBundleManifest};
|
|
1
|
+
import{APP_ASSET_MAX_BYTES as e,APP_DESCRIPTOR_PATH as t,APP_PART_PREFIX as n,ATTESTATION_TAG as r,BUNDLE_MANIFEST_ROUTE as i,BUNDLE_MANIFEST_VERSION as a,NATIVE_COMPOSITION_MAX_BYTES as o,NATIVE_ENTRY_CONTENT_TYPE as s,NATIVE_ENTRY_PATH as c,WEB_ENTRY_PATH as l,assetFor as u,attestationBytes as d,canonicalManifest as f,digestFor as p,isBundleManifest as m,isSignedBundleManifest as h}from"./types/bundleManifest.mjs";import{CLIENT_TO_SERVER_INFO as g,MAX_MESSAGES_PER_KEY as _,NONCE_BYTES as v,NONCE_COUNTER_BYTES as y,NONCE_PREFIX_BYTES as b,SEALED_BODY_HEADER as x,SEALED_CLIENT_KEY_HEADER as S,SEALED_KEY_PARAM as C,SEALED_VERSION as w,SERVER_TO_CLIENT_INFO as T,describeSealedFailure as E,infoFor as D,isSealedEnvelope as O}from"./types/sealedChannel.mjs";import{createSerialQueue as k}from"./services/serialQueue/index.mjs";import{FEDERATION_FRESHNESS as A,FEDERATION_TAGS as j,federationEnvelopeFields as M}from"./types/federation.mjs";export{e as APP_ASSET_MAX_BYTES,t as APP_DESCRIPTOR_PATH,n as APP_PART_PREFIX,r as ATTESTATION_TAG,i as BUNDLE_MANIFEST_ROUTE,a as BUNDLE_MANIFEST_VERSION,g as CLIENT_TO_SERVER_INFO,A as FEDERATION_FRESHNESS,j as FEDERATION_TAGS,_ as MAX_MESSAGES_PER_KEY,o as NATIVE_COMPOSITION_MAX_BYTES,s as NATIVE_ENTRY_CONTENT_TYPE,c as NATIVE_ENTRY_PATH,v as NONCE_BYTES,y as NONCE_COUNTER_BYTES,b as NONCE_PREFIX_BYTES,x as SEALED_BODY_HEADER,S as SEALED_CLIENT_KEY_HEADER,C as SEALED_KEY_PARAM,w as SEALED_VERSION,T as SERVER_TO_CLIENT_INFO,l as WEB_ENTRY_PATH,u as assetFor,d as attestationBytes,f as canonicalManifest,k as createSerialQueue,E as describeSealedFailure,p as digestFor,M as federationEnvelopeFields,D as infoFor,m as isBundleManifest,O as isSealedEnvelope,h as isSignedBundleManifest};
|
package/dist/node.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/deviceSignature/index.cjs"),t=require("./services/federationEnvelope/index.cjs"),n=require("./services/nodeSealedChannel/index.cjs"),r=require("./services/bundleSigner/index.cjs");exports.createBundleSigner=r.createBundleSigner,exports.createClientHandshake=n.createClientHandshake,exports.createHostHandshake=n.createHostHandshake,exports.publicKeyOf=r.publicKeyOf,exports.signEnvelope=t.signEnvelope,exports.verifyDeviceSignature=e.verifyDeviceSignature,exports.verifyEnvelope=t.verifyEnvelope;
|
package/dist/node.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { verifyDeviceSignature } from "./services/deviceSignature/index.mjs";
|
|
2
|
+
import { signEnvelope, verifyEnvelope } from "./services/federationEnvelope/index.mjs";
|
|
1
3
|
import { ClientHandshake, HostHandshake, OpenResult, SealResult, SealedChannel, createClientHandshake, createHostHandshake } from "./services/nodeSealedChannel/index.mjs";
|
|
2
4
|
import { BundleSigner, createBundleSigner, publicKeyOf } from "./services/bundleSigner/index.mjs";
|
|
3
|
-
|
|
4
|
-
export { type BundleSigner, type ClientHandshake, type HostHandshake, type OpenResult, type SealResult, type SealedChannel, createBundleSigner, createClientHandshake, createHostHandshake, publicKeyOf, verifyDeviceSignature };
|
|
5
|
+
export { type BundleSigner, type ClientHandshake, type HostHandshake, type OpenResult, type SealResult, type SealedChannel, createBundleSigner, createClientHandshake, createHostHandshake, publicKeyOf, signEnvelope, verifyDeviceSignature, verifyEnvelope };
|
package/dist/node.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{verifyDeviceSignature as e}from"./services/deviceSignature/index.mjs";import{signEnvelope as t,verifyEnvelope as n}from"./services/federationEnvelope/index.mjs";import{createClientHandshake as r,createHostHandshake as i}from"./services/nodeSealedChannel/index.mjs";import{createBundleSigner as a,publicKeyOf as o}from"./services/bundleSigner/index.mjs";export{a as createBundleSigner,r as createClientHandshake,i as createHostHandshake,o as publicKeyOf,t as signEnvelope,e as verifyDeviceSignature,n as verifyEnvelope};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../types/bundleManifest.cjs");let t=require("node:crypto");const n=/^[A-Za-z0-9_-]+$/u;function r(e,t){return typeof e==`string`&&e.length>0&&e.length<=t&&n.test(e)}function i(e){try{let n=(0,t.createPublicKey)({key:Buffer.from(e,`base64url`),format:`der`,type:`spki`});return n.asymmetricKeyType!==`ec`||n.asymmetricKeyDetails?.namedCurve!==`prime256v1`?void 0:n}catch{return}}function a(n,a,o,s){let c=e.attestationBytes(a,o);if(!r(n,512)||!r(s,86)||s.length!==86)return!1;let l=i(n);if(l===void 0)return!1;try{return(0,t.verify)(`sha256`,c,{key:l,dsaEncoding:`ieee-p1363`},Buffer.from(s,`base64url`))}catch{return!1}}exports.verifyDeviceSignature=a;
|
|
1
|
+
const e=require("../../types/bundleManifest.cjs");let t=require("node:crypto");const n=/^[A-Za-z0-9_-]+$/u;function r(e,t){return typeof e==`string`&&e.length>0&&e.length<=t&&n.test(e)}function i(e){try{let n=(0,t.createPublicKey)({key:Buffer.from(e,`base64url`),format:`der`,type:`spki`});return n.asymmetricKeyType!==`ec`||n.asymmetricKeyDetails?.namedCurve!==`prime256v1`?void 0:n}catch{return}}function a(n,a,o,s){let c=e.attestationBytes(a,o);if(!r(n,512)||!r(s,86)||s.length!==86)return!1;let l=i(n);if(l===void 0)return!1;try{return(0,t.verify)(`sha256`,c,{key:l.export({format:`pem`,type:`spki`}),dsaEncoding:`ieee-p1363`},Buffer.from(s,`base64url`))}catch{return!1}}exports.verifyDeviceSignature=a;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["createPublicKey","attestationBytes","verify","require_bundleManifest","node_crypto","BASE64URL","isBase64Url","value","maximum","p256Key","key","spki","verifyDeviceSignature","message","tag","fields","signature"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(SIGN_ALGORITHM
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["createPublicKey","attestationBytes","verify","require_bundleManifest","node_crypto","BASE64URL","isBase64Url","value","maximum","p256Key","key","spki","verifyDeviceSignature","message","tag","fields","signature"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(\n SIGN_ALGORITHM,\n message,\n { key: key.export({ format: 'pem', type: 'spki' }), dsaEncoding: 'ieee-p1363' },\n Buffer.from(signature, 'base64url'),\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"+EAIA,MAIMK,EAAY,oBAElB,SAASC,EAAY,EAAgB,EAAkC,CACrE,OAAO,OAAOC,GAAU,UAAYA,EAAM,OAAS,GAAKA,EAAM,QAAUC,GAAWH,EAAU,KAAKE,CAAK,CACzG,CAGA,SAASE,EAAQ,EAAqC,CACpD,GAAI,CACF,IAAMC,GAAA,EAAMV,EAAAA,gBAAAA,CAAgB,CAAE,IAAK,OAAO,KAAKW,EAAM,WAAW,EAAG,OAAQ,MAAO,KAAM,MAAO,CAAC,EAEhG,OADID,EAAI,oBAAsB,MAAQA,EAAI,sBAAsB,aAAe,aAAO,OAC/EA,CACT,MAAQ,CACN,MACF,CACF,CAOA,SAAgBE,EACd,EACA,EACA,EACA,EACS,CACT,IAAMC,EAAUZ,EAAAA,iBAAiBa,EAAKC,CAAM,EAE5C,GADI,CAACT,EAAYK,EAAM,GAAe,GAClC,CAACL,EAAYU,EAAW,EAAsB,GAAKA,EAAU,SAAW,GAAwB,MAAO,GAC3G,IAAMN,EAAMD,EAAQE,CAAI,EACxB,GAAID,IAAQ,IAAA,GAAW,MAAO,GAC9B,GAAI,CACF,OAAA,EAAOR,EAAAA,OAAAA,CACL,SACAW,EACA,CAAE,IAAKH,EAAI,OAAO,CAAE,OAAQ,MAAO,KAAM,MAAO,CAAC,EAAG,YAAa,YAAa,EAC9E,OAAO,KAAKM,EAAW,WAAW,CACpC,CACF,MAAQ,CACN,MAAO,EACT,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{attestationBytes as e}from"../../types/bundleManifest.mjs";import{createPublicKey as t,verify as n}from"node:crypto";const r=/^[A-Za-z0-9_-]+$/u;function i(e,t){return typeof e==`string`&&e.length>0&&e.length<=t&&r.test(e)}function a(e){try{let n=t({key:Buffer.from(e,`base64url`),format:`der`,type:`spki`});return n.asymmetricKeyType!==`ec`||n.asymmetricKeyDetails?.namedCurve!==`prime256v1`?void 0:n}catch{return}}function o(t,r,o,s){let c=e(r,o);if(!i(t,512)||!i(s,86)||s.length!==86)return!1;let l=a(t);if(l===void 0)return!1;try{return n(`sha256`,c,{key:l,dsaEncoding:`ieee-p1363`},Buffer.from(s,`base64url`))}catch{return!1}}export{o as verifyDeviceSignature};
|
|
1
|
+
import{attestationBytes as e}from"../../types/bundleManifest.mjs";import{createPublicKey as t,verify as n}from"node:crypto";const r=/^[A-Za-z0-9_-]+$/u;function i(e,t){return typeof e==`string`&&e.length>0&&e.length<=t&&r.test(e)}function a(e){try{let n=t({key:Buffer.from(e,`base64url`),format:`der`,type:`spki`});return n.asymmetricKeyType!==`ec`||n.asymmetricKeyDetails?.namedCurve!==`prime256v1`?void 0:n}catch{return}}function o(t,r,o,s){let c=e(r,o);if(!i(t,512)||!i(s,86)||s.length!==86)return!1;let l=a(t);if(l===void 0)return!1;try{return n(`sha256`,c,{key:l.export({format:`pem`,type:`spki`}),dsaEncoding:`ieee-p1363`},Buffer.from(s,`base64url`))}catch{return!1}}export{o as verifyDeviceSignature};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["BASE64URL","isBase64Url","value","maximum","p256Key","key","createPublicKey","spki","verifyDeviceSignature","message","attestationBytes","tag","fields","signature","verify"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(SIGN_ALGORITHM
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["BASE64URL","isBase64Url","value","maximum","p256Key","key","createPublicKey","spki","verifyDeviceSignature","message","attestationBytes","tag","fields","signature","verify"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(\n SIGN_ALGORITHM,\n message,\n { key: key.export({ format: 'pem', type: 'spki' }), dsaEncoding: 'ieee-p1363' },\n Buffer.from(signature, 'base64url'),\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"4HAIA,MAIMA,EAAY,oBAElB,SAASC,EAAY,EAAgB,EAAkC,CACrE,OAAO,OAAOC,GAAU,UAAYA,EAAM,OAAS,GAAKA,EAAM,QAAUC,GAAWH,EAAU,KAAKE,CAAK,CACzG,CAGA,SAASE,EAAQ,EAAqC,CACpD,GAAI,CACF,IAAMC,EAAMC,EAAgB,CAAE,IAAK,OAAO,KAAKC,EAAM,WAAW,EAAG,OAAQ,MAAO,KAAM,MAAO,CAAC,EAEhG,OADIF,EAAI,oBAAsB,MAAQA,EAAI,sBAAsB,aAAe,aAAO,OAC/EA,CACT,MAAQ,CACN,MACF,CACF,CAOA,SAAgBG,EACd,EACA,EACA,EACA,EACS,CACT,IAAMC,EAAUC,EAAiBC,EAAKC,CAAM,EAE5C,GADI,CAACX,EAAYM,EAAM,GAAe,GAClC,CAACN,EAAYY,EAAW,EAAsB,GAAKA,EAAU,SAAW,GAAwB,MAAO,GAC3G,IAAMR,EAAMD,EAAQG,CAAI,EACxB,GAAIF,IAAQ,IAAA,GAAW,MAAO,GAC9B,GAAI,CACF,OAAOS,EACL,SACAL,EACA,CAAE,IAAKJ,EAAI,OAAO,CAAE,OAAQ,MAAO,KAAM,MAAO,CAAC,EAAG,YAAa,YAAa,EAC9E,OAAO,KAAKQ,EAAW,WAAW,CACpC,CACF,MAAQ,CACN,MAAO,EACT,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../types/bundleManifest.cjs"),t=require("../../types/federation.cjs"),n=require("../deviceSignature/index.cjs");let r=require("node:crypto");function i(e,n,r){let{issuedAt:i,expiresAt:a,notBefore:o}=n;if(![r,i,a].every(e=>Number.isSafeInteger(e)&&e>=0)||a<=i||i>r+t.FEDERATION_FRESHNESS.clockSkew||r>=a-t.FEDERATION_FRESHNESS.clockSkew||o!==void 0&&(!Number.isSafeInteger(o)||o<i||o>=a||r<o))return!1;let s=e===`po/request/v1`||e===`po/host-request/v1`?t.FEDERATION_FRESHNESS.envelope:e===`po/grant/v1`?t.FEDERATION_FRESHNESS.grant:e===`po/capabilities/v1`?t.FEDERATION_FRESHNESS.manifest:void 0;return s===void 0||a-i<=s}function a(n,i,a){if(!t.FEDERATION_TAGS.includes(i))throw Error(`Unknown federation tag`);if(n.type!==`private`||n.asymmetricKeyType!==`ec`||n.asymmetricKeyDetails?.namedCurve!==`prime256v1`)throw Error(`Envelope signer must be a P-256 private key`);return{tag:i,payload:a,signature:(0,r.sign)(`sha256`,e.attestationBytes(i,t.federationEnvelopeFields(a)),{key:n.export({format:`pem`,type:`pkcs8`}),dsaEncoding:`ieee-p1363`}).toString(`base64url`)}}function o(e,r,a,o){try{return!t.FEDERATION_TAGS.includes(r)||a.tag!==r||Object.keys(a).sort().join(`,`)!==`payload,signature,tag`||!i(r,a.payload,o)?!1:n.verifyDeviceSignature(e,r,t.federationEnvelopeFields(a.payload),a.signature)}catch{return!1}}exports.signEnvelope=a,exports.verifyEnvelope=o;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["FEDERATION_FRESHNESS","FEDERATION_TAGS","sign","attestationBytes","federationEnvelopeFields","verifyDeviceSignature","require_bundleManifest","require_federation","require_index","node_crypto","validTime","payload","now","issuedAt","expiresAt","value","notBefore","maximum","tag","signEnvelope","privateKey","verifyEnvelope","expectedTag","envelope","spki"],"sources":["../../../src/services/federationEnvelope/index.ts"],"sourcesContent":["import { type KeyObject, sign } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\nimport {\n FEDERATION_FRESHNESS,\n federationEnvelopeFields,\n FEDERATION_TAGS,\n type FederationTag,\n type FederationValidity,\n type SignedFederationEnvelope,\n} from '../../types/federation';\nimport { verifyDeviceSignature } from '../deviceSignature';\n\nfunction validTime(tag: FederationTag, payload: FederationValidity, now: number): boolean {\n const { issuedAt, expiresAt, notBefore } = payload;\n if (![now, issuedAt, expiresAt].every((value) => Number.isSafeInteger(value) && value >= 0)) return false;\n if (expiresAt <= issuedAt || issuedAt > now + FEDERATION_FRESHNESS.clockSkew) return false;\n // G8: skew is a conservative deadline, never grace after expiry.\n if (now >= expiresAt - FEDERATION_FRESHNESS.clockSkew) return false;\n if (\n notBefore !== undefined &&\n (!Number.isSafeInteger(notBefore) || notBefore < issuedAt || notBefore >= expiresAt || now < notBefore)\n )\n return false;\n const maximum =\n tag === 'po/request/v1' || tag === 'po/host-request/v1'\n ? FEDERATION_FRESHNESS.envelope\n : tag === 'po/grant/v1'\n ? FEDERATION_FRESHNESS.grant\n : tag === 'po/capabilities/v1'\n ? FEDERATION_FRESHNESS.manifest\n : undefined;\n return maximum === undefined || expiresAt - issuedAt <= maximum;\n}\n\n/** Signs P-256 attestation bytes. This is not a JWT issuer or an authorization decision. */\nexport function signEnvelope<T extends FederationValidity, Tag extends FederationTag>(\n privateKey: KeyObject,\n tag: Tag,\n payload: T,\n): SignedFederationEnvelope<T, Tag> {\n if (!FEDERATION_TAGS.includes(tag)) throw new Error('Unknown federation tag');\n if (\n privateKey.type !== 'private' ||\n privateKey.asymmetricKeyType !== 'ec' ||\n privateKey.asymmetricKeyDetails?.namedCurve !== 'prime256v1'\n ) {\n throw new Error('Envelope signer must be a P-256 private key');\n }\n const signature = sign('sha256', attestationBytes(tag, federationEnvelopeFields(payload)), {\n // Workerd accepts encoded keys here, but not a KeyObject nested in options.\n key: privateKey.export({ format: 'pem', type: 'pkcs8' }),\n dsaEncoding: 'ieee-p1363',\n }).toString('base64url');\n return { tag, payload, signature };\n}\n\n/**\n * Fail-closed signature and freshness check. Callers must validate the contract shape,\n * bind the trusted key and expected tag, check all authorization scopes and durably admit nonces.\n * No roster, JWKS, lease or purge window is defined here.\n */\nexport function verifyEnvelope<T extends FederationValidity>(\n spki: string,\n expectedTag: FederationTag,\n envelope: SignedFederationEnvelope<T>,\n now: number,\n): boolean {\n try {\n if (!FEDERATION_TAGS.includes(expectedTag) || envelope.tag !== expectedTag) return false;\n if (Object.keys(envelope).sort().join(',') !== 'payload,signature,tag') return false;\n if (!validTime(expectedTag, envelope.payload, now)) return false;\n return verifyDeviceSignature(spki, expectedTag, federationEnvelopeFields(envelope.payload), envelope.signature);\n } catch {\n // Untrusted malformed JSON, key or signature is a failed proof, never an exception to admission.\n return false;\n }\n}\n"],"mappings":"iKAaA,SAASU,EAAU,EAAoB,EAA6B,EAAsB,CACxF,GAAM,CAAE,WAAU,YAAW,aAAcC,EAK3C,GAJI,CAAC,CAACC,EAAKC,EAAUC,CAAS,CAAC,CAAC,MAAO,GAAU,OAAO,cAAcC,CAAK,GAAKA,GAAS,CAAC,GACtFD,GAAaD,GAAYA,EAAWD,EAAMZ,EAAAA,qBAAqB,WAE/DY,GAAOE,EAAYd,EAAAA,qBAAqB,WAE1CgB,IAAc,IAAA,KACb,CAAC,OAAO,cAAcA,CAAS,GAAKA,EAAYH,GAAYG,GAAaF,GAAaF,EAAMI,GAE7F,MAAO,GACT,IAAMC,EACJC,IAAQ,iBAAmBA,IAAQ,qBAC/BlB,EAAAA,qBAAqB,SACrBkB,IAAQ,cACNlB,EAAAA,qBAAqB,MACrBkB,IAAQ,qBACNlB,EAAAA,qBAAqB,SACrB,IAAA,GACV,OAAOiB,IAAY,IAAA,IAAaH,EAAYD,GAAYI,CAC1D,CAGA,SAAgBE,EACd,EACA,EACA,EACkC,CAClC,GAAI,CAAClB,EAAAA,gBAAgB,SAASiB,CAAG,EAAG,MAAU,MAAM,wBAAwB,EAC5E,GACEE,EAAW,OAAS,WACpBA,EAAW,oBAAsB,MACjCA,EAAW,sBAAsB,aAAe,aAEhD,MAAU,MAAM,6CAA6C,EAO/D,MAAO,CAAE,MAAK,UAAS,WAAA,EALLlB,EAAAA,KAAAA,CAAK,SAAUC,EAAAA,iBAAiBe,EAAKd,EAAAA,yBAAyBO,CAAO,CAAC,EAAG,CAEzF,IAAKS,EAAW,OAAO,CAAE,OAAQ,MAAO,KAAM,OAAQ,CAAC,EACvD,YAAa,YACf,CAAC,CAAC,CAAC,SAAS,WACmB,CAAE,CACnC,CAOA,SAAgBC,EACd,EACA,EACA,EACA,EACS,CACT,GAAI,CAIF,MAHI,CAACpB,EAAAA,gBAAgB,SAASqB,CAAW,GAAKC,EAAS,MAAQD,GAC3D,OAAO,KAAKC,CAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAM,yBAC3C,CAACb,EAAUY,EAAaC,EAAS,QAASX,CAAG,EAAU,GACpDP,EAAAA,sBAAsBmB,EAAMF,EAAalB,EAAAA,yBAAyBmB,EAAS,OAAO,EAAGA,EAAS,SAAS,CAChH,MAAQ,CAEN,MAAO,EACT,CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FederationTag, FederationValidity, SignedFederationEnvelope } from "../../types/federation.mjs";
|
|
2
|
+
import { KeyObject } from "node:crypto";
|
|
3
|
+
//#region src/services/federationEnvelope/index.d.ts
|
|
4
|
+
/** Signs P-256 attestation bytes. This is not a JWT issuer or an authorization decision. */
|
|
5
|
+
export declare function signEnvelope<T extends FederationValidity, Tag extends FederationTag>(privateKey: KeyObject, tag: Tag, payload: T): SignedFederationEnvelope<T, Tag>;
|
|
6
|
+
/**
|
|
7
|
+
* Fail-closed signature and freshness check. Callers must validate the contract shape,
|
|
8
|
+
* bind the trusted key and expected tag, check all authorization scopes and durably admit nonces.
|
|
9
|
+
* No roster, JWKS, lease or purge window is defined here.
|
|
10
|
+
*/
|
|
11
|
+
export declare function verifyEnvelope<T extends FederationValidity>(spki: string, expectedTag: FederationTag, envelope: SignedFederationEnvelope<T>, now: number): boolean;
|
|
12
|
+
//#endregion
|
|
13
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/federationEnvelope/index.ts"],"mappings":";;;;wBAoCgB,aAAa,UAAU,oBAAoB,YAAY,eACrE,YAAY,WACZ,KAAK,KACL,SAAS,IACR,yBAAyB,GAAG;;;;;;wBAsBf,eAAe,UAAU,oBACvC,cACA,aAAa,eACb,UAAU,yBAAyB,IACnC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{attestationBytes as e}from"../../types/bundleManifest.mjs";import{FEDERATION_FRESHNESS as t,FEDERATION_TAGS as n,federationEnvelopeFields as r}from"../../types/federation.mjs";import{verifyDeviceSignature as i}from"../deviceSignature/index.mjs";import{sign as a}from"node:crypto";function o(e,n,r){let{issuedAt:i,expiresAt:a,notBefore:o}=n;if(![r,i,a].every(e=>Number.isSafeInteger(e)&&e>=0)||a<=i||i>r+t.clockSkew||r>=a-t.clockSkew||o!==void 0&&(!Number.isSafeInteger(o)||o<i||o>=a||r<o))return!1;let s=e===`po/request/v1`||e===`po/host-request/v1`?t.envelope:e===`po/grant/v1`?t.grant:e===`po/capabilities/v1`?t.manifest:void 0;return s===void 0||a-i<=s}function s(t,i,o){if(!n.includes(i))throw Error(`Unknown federation tag`);if(t.type!==`private`||t.asymmetricKeyType!==`ec`||t.asymmetricKeyDetails?.namedCurve!==`prime256v1`)throw Error(`Envelope signer must be a P-256 private key`);return{tag:i,payload:o,signature:a(`sha256`,e(i,r(o)),{key:t.export({format:`pem`,type:`pkcs8`}),dsaEncoding:`ieee-p1363`}).toString(`base64url`)}}function c(e,t,a,s){try{return!n.includes(t)||a.tag!==t||Object.keys(a).sort().join(`,`)!==`payload,signature,tag`||!o(t,a.payload,s)?!1:i(e,t,r(a.payload),a.signature)}catch{return!1}}export{s as signEnvelope,c as verifyEnvelope};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["validTime","payload","now","issuedAt","expiresAt","value","FEDERATION_FRESHNESS","notBefore","maximum","tag","signEnvelope","FEDERATION_TAGS","privateKey","sign","attestationBytes","federationEnvelopeFields","verifyEnvelope","expectedTag","envelope","verifyDeviceSignature","spki"],"sources":["../../../src/services/federationEnvelope/index.ts"],"sourcesContent":["import { type KeyObject, sign } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\nimport {\n FEDERATION_FRESHNESS,\n federationEnvelopeFields,\n FEDERATION_TAGS,\n type FederationTag,\n type FederationValidity,\n type SignedFederationEnvelope,\n} from '../../types/federation';\nimport { verifyDeviceSignature } from '../deviceSignature';\n\nfunction validTime(tag: FederationTag, payload: FederationValidity, now: number): boolean {\n const { issuedAt, expiresAt, notBefore } = payload;\n if (![now, issuedAt, expiresAt].every((value) => Number.isSafeInteger(value) && value >= 0)) return false;\n if (expiresAt <= issuedAt || issuedAt > now + FEDERATION_FRESHNESS.clockSkew) return false;\n // G8: skew is a conservative deadline, never grace after expiry.\n if (now >= expiresAt - FEDERATION_FRESHNESS.clockSkew) return false;\n if (\n notBefore !== undefined &&\n (!Number.isSafeInteger(notBefore) || notBefore < issuedAt || notBefore >= expiresAt || now < notBefore)\n )\n return false;\n const maximum =\n tag === 'po/request/v1' || tag === 'po/host-request/v1'\n ? FEDERATION_FRESHNESS.envelope\n : tag === 'po/grant/v1'\n ? FEDERATION_FRESHNESS.grant\n : tag === 'po/capabilities/v1'\n ? FEDERATION_FRESHNESS.manifest\n : undefined;\n return maximum === undefined || expiresAt - issuedAt <= maximum;\n}\n\n/** Signs P-256 attestation bytes. This is not a JWT issuer or an authorization decision. */\nexport function signEnvelope<T extends FederationValidity, Tag extends FederationTag>(\n privateKey: KeyObject,\n tag: Tag,\n payload: T,\n): SignedFederationEnvelope<T, Tag> {\n if (!FEDERATION_TAGS.includes(tag)) throw new Error('Unknown federation tag');\n if (\n privateKey.type !== 'private' ||\n privateKey.asymmetricKeyType !== 'ec' ||\n privateKey.asymmetricKeyDetails?.namedCurve !== 'prime256v1'\n ) {\n throw new Error('Envelope signer must be a P-256 private key');\n }\n const signature = sign('sha256', attestationBytes(tag, federationEnvelopeFields(payload)), {\n // Workerd accepts encoded keys here, but not a KeyObject nested in options.\n key: privateKey.export({ format: 'pem', type: 'pkcs8' }),\n dsaEncoding: 'ieee-p1363',\n }).toString('base64url');\n return { tag, payload, signature };\n}\n\n/**\n * Fail-closed signature and freshness check. Callers must validate the contract shape,\n * bind the trusted key and expected tag, check all authorization scopes and durably admit nonces.\n * No roster, JWKS, lease or purge window is defined here.\n */\nexport function verifyEnvelope<T extends FederationValidity>(\n spki: string,\n expectedTag: FederationTag,\n envelope: SignedFederationEnvelope<T>,\n now: number,\n): boolean {\n try {\n if (!FEDERATION_TAGS.includes(expectedTag) || envelope.tag !== expectedTag) return false;\n if (Object.keys(envelope).sort().join(',') !== 'payload,signature,tag') return false;\n if (!validTime(expectedTag, envelope.payload, now)) return false;\n return verifyDeviceSignature(spki, expectedTag, federationEnvelopeFields(envelope.payload), envelope.signature);\n } catch {\n // Untrusted malformed JSON, key or signature is a failed proof, never an exception to admission.\n return false;\n }\n}\n"],"mappings":"+RAaA,SAASA,EAAU,EAAoB,EAA6B,EAAsB,CACxF,GAAM,CAAE,WAAU,YAAW,aAAcC,EAK3C,GAJI,CAAC,CAACC,EAAKC,EAAUC,CAAS,CAAC,CAAC,MAAO,GAAU,OAAO,cAAcC,CAAK,GAAKA,GAAS,CAAC,GACtFD,GAAaD,GAAYA,EAAWD,EAAMI,EAAqB,WAE/DJ,GAAOE,EAAYE,EAAqB,WAE1CC,IAAc,IAAA,KACb,CAAC,OAAO,cAAcA,CAAS,GAAKA,EAAYJ,GAAYI,GAAaH,GAAaF,EAAMK,GAE7F,MAAO,GACT,IAAMC,EACJC,IAAQ,iBAAmBA,IAAQ,qBAC/BH,EAAqB,SACrBG,IAAQ,cACNH,EAAqB,MACrBG,IAAQ,qBACNH,EAAqB,SACrB,IAAA,GACV,OAAOE,IAAY,IAAA,IAAaJ,EAAYD,GAAYK,CAC1D,CAGA,SAAgBE,EACd,EACA,EACA,EACkC,CAClC,GAAI,CAACC,EAAgB,SAASF,CAAG,EAAG,MAAU,MAAM,wBAAwB,EAC5E,GACEG,EAAW,OAAS,WACpBA,EAAW,oBAAsB,MACjCA,EAAW,sBAAsB,aAAe,aAEhD,MAAU,MAAM,6CAA6C,EAO/D,MAAO,CAAE,MAAK,UAAS,UALLC,EAAK,SAAUC,EAAiBL,EAAKM,EAAyBd,CAAO,CAAC,EAAG,CAEzF,IAAKW,EAAW,OAAO,CAAE,OAAQ,MAAO,KAAM,OAAQ,CAAC,EACvD,YAAa,YACf,CAAC,CAAC,CAAC,SAAS,WACmB,CAAE,CACnC,CAOA,SAAgBI,EACd,EACA,EACA,EACA,EACS,CACT,GAAI,CAIF,MAHI,CAACL,EAAgB,SAASM,CAAW,GAAKC,EAAS,MAAQD,GAC3D,OAAO,KAAKC,CAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAM,yBAC3C,CAAClB,EAAUiB,EAAaC,EAAS,QAAShB,CAAG,EAAU,GACpDiB,EAAsBC,EAAMH,EAAaF,EAAyBG,EAAS,OAAO,EAAGA,EAAS,SAAS,CAChH,MAAQ,CAEN,MAAO,EACT,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../types/federation.cjs"),t=require("../federationVerifier/index.cjs");function n(n){async function r(e,r,i){if(!t.isFederationIssuerOrigin(e)||(await n.issuer(e))?.issuer!==e)throw Error(`Unpinned issuer`);let a=await n.fetch(`${e}${r}`,{method:i===void 0?`GET`:`POST`,redirect:`manual`,cache:`no-store`,credentials:`omit`,headers:{accept:`application/json`,...i===void 0?{}:{"content-type":`application/json`}},...i===void 0?{}:{body:JSON.stringify(i)}});if(!a.ok||a.redirected||a.url&&new URL(a.url).origin!==e)throw Error(`Issuer request refused`);return a.json()}let i=Promise.resolve();async function a(e){let t=structuredClone(await n.feed(e));if(!Number.isSafeInteger(t.cursor)||t.cursor<0)throw Error(`Invalid feed cursor`);for(;;){let n=await r(e,`/api/auth/revocations?after=${t.cursor}`);if(!Array.isArray(n)||n.length>1e3)throw Error(`Invalid issuer feed`);for(let e of n){let n=e;if(!n||!Number.isSafeInteger(n.sequence)||n.sequence<=t.cursor||typeof n.subject!=`string`||!n.subject||n.deviceId!==null&&typeof n.deviceId!=`string`||!Number.isSafeInteger(n.createdAt)||n.createdAt<0||n.epoch!==null&&(!Number.isSafeInteger(n.epoch)||n.epoch<0))throw Error(`Invalid revocation row`);t.records.push(n),t.cursor=n.sequence}if(n.length<1e3)break}return t.syncedAt=n.now(),await n.saveFeed(e,t),t}return{async fetchJwks(e){let t=await r(e,`/api/auth/jwks`);if(!t||!Array.isArray(t.keys)||!t.keys.every(e=>e&&e.kty===`EC`&&e.crv===`P-256`&&typeof e.kid==`string`&&typeof e.x==`string`&&typeof e.y==`string`))throw Error(`Invalid issuer JWKS`);return t},async lookupDevice(e,t){if(!n.deviceKeyProof)throw Error(`Enrolled host proof required`);let i=await n.deviceKeyProof(e.issuer,t);if(i.payload.issuer!==e.issuer||i.payload.keyId!==t)throw Error(`Wrong key proof`);let a=await r(e.issuer,`/api/auth/device-key`,{proof:i});if(!a||a.issuer!==e.issuer||a.subject!==e.subject||a.keyId!==t||typeof a.publicKey!=`string`||typeof a.revoked!=`boolean`||![`tofu`,`approved`,`hub`,`recovered`,`org-admin`].includes(a.trust)||a.organizationId!==null&&typeof a.organizationId!=`string`||!Number.isSafeInteger(a.revocationEpoch)||a.revocationEpoch<0)throw Error(`Invalid device authority`);return{devicePublicKey:a.publicKey,deviceRevoked:a.revoked,revocationEpoch:a.revocationEpoch,trust:a.trust,organizationId:a.organizationId}},async checkAuthority(t,r,o){let s=i.then(()=>a(t.issuer));i=s.catch(()=>void 0);let c;try{c=await s}catch(r){if(c=await n.feed(t.issuer),c.syncedAt===void 0||n.now()<c.syncedAt||n.now()-c.syncedAt>e.FEDERATION_FRESHNESS.token)throw r}let l={...o};for(let e of c.records)e.subject===t.subject&&(e.epoch===null?e.deviceId===null&&(l.deviceRevoked=!0):l.revocationEpoch=Math.max(l.revocationEpoch,e.epoch),e.deviceId===r&&(l.deviceRevoked=!0));return l}}}exports.createFederationIssuerHttp=n;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["isFederationIssuerOrigin","FEDERATION_FRESHNESS","require_federation","require_index","createFederationIssuerHttp","get","issuer","ports","response","path","body","queue","sync","state","page","raw","row","value","key","proof","person","keyId","result","error","current","authority","deviceKeyId"],"sources":["../../../src/services/federationIssuerHttp/index.ts"],"sourcesContent":["import { FEDERATION_FRESHNESS } from '../../types/federation';\nimport { isFederationIssuerOrigin } from '../federationVerifier';\nimport type { FederationAuthority, FederationIssuerPin, FederationPrincipal } from '../federationVerifier/type';\nimport type { FederationDeviceKeyResponse, FederationIssuerHttpPorts, FederationRevocation } from './type';\n\n/** Real HTTP transport for the issuer's published routes. Redirects never escape a pin. */\nexport function createFederationIssuerHttp(ports: FederationIssuerHttpPorts) {\n async function get(issuer: string, path: string, body?: unknown): Promise<unknown> {\n if (!isFederationIssuerOrigin(issuer) || (await ports.issuer(issuer))?.issuer !== issuer)\n throw new Error('Unpinned issuer');\n const response = await ports.fetch(`${issuer}${path}`, {\n method: body === undefined ? 'GET' : 'POST',\n redirect: 'manual',\n cache: 'no-store',\n credentials: 'omit',\n headers: { accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }) },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok || response.redirected || (response.url && new URL(response.url).origin !== issuer))\n throw new Error('Issuer request refused');\n return response.json();\n }\n let queue: Promise<unknown> = Promise.resolve();\n async function sync(issuer: string) {\n const state = structuredClone(await ports.feed(issuer));\n if (!Number.isSafeInteger(state.cursor) || state.cursor < 0) throw new Error('Invalid feed cursor');\n // Persist only after a complete fetch, never accept a partial or failed page.\n for (;;) {\n const page = await get(issuer, `/api/auth/revocations?after=${state.cursor}`);\n if (!Array.isArray(page) || page.length > 1000) throw new Error('Invalid issuer feed');\n for (const raw of page) {\n const row = raw as FederationRevocation;\n if (\n !row ||\n !Number.isSafeInteger(row.sequence) ||\n row.sequence <= state.cursor ||\n typeof row.subject !== 'string' ||\n !row.subject ||\n (row.deviceId !== null && typeof row.deviceId !== 'string') ||\n !Number.isSafeInteger(row.createdAt) ||\n row.createdAt < 0 ||\n (row.epoch !== null && (!Number.isSafeInteger(row.epoch) || row.epoch < 0))\n )\n throw new Error('Invalid revocation row');\n state.records.push(row);\n state.cursor = row.sequence;\n }\n if (page.length < 1000) break;\n }\n state.syncedAt = ports.now();\n await ports.saveFeed(issuer, state);\n return state;\n }\n return {\n async fetchJwks(issuer: string): Promise<FederationIssuerPin['jwks']> {\n const value = (await get(issuer, '/api/auth/jwks')) as FederationIssuerPin['jwks'];\n if (\n !value ||\n !Array.isArray(value.keys) ||\n !value.keys.every(\n (key) =>\n key &&\n key.kty === 'EC' &&\n key.crv === 'P-256' &&\n typeof key.kid === 'string' &&\n typeof key.x === 'string' &&\n typeof key.y === 'string',\n )\n )\n throw new Error('Invalid issuer JWKS');\n return value;\n },\n /** Host-authenticated key lookup, never a key supplied in an envelope. */\n async lookupDevice(person: FederationPrincipal, keyId: string): Promise<FederationAuthority> {\n if (!ports.deviceKeyProof) throw new Error('Enrolled host proof required');\n const proof = await ports.deviceKeyProof(person.issuer, keyId);\n if (proof.payload.issuer !== person.issuer || proof.payload.keyId !== keyId) throw new Error('Wrong key proof');\n const row = (await get(person.issuer, '/api/auth/device-key', { proof })) as FederationDeviceKeyResponse;\n if (\n !row ||\n row.issuer !== person.issuer ||\n row.subject !== person.subject ||\n row.keyId !== keyId ||\n typeof row.publicKey !== 'string' ||\n typeof row.revoked !== 'boolean' ||\n !['tofu', 'approved', 'hub', 'recovered', 'org-admin'].includes(row.trust) ||\n (row.organizationId !== null && typeof row.organizationId !== 'string') ||\n !Number.isSafeInteger(row.revocationEpoch) ||\n row.revocationEpoch < 0\n )\n throw new Error('Invalid device authority');\n return {\n devicePublicKey: row.publicKey,\n deviceRevoked: row.revoked,\n revocationEpoch: row.revocationEpoch,\n trust: row.trust,\n organizationId: row.organizationId,\n };\n },\n /** Feed only removes authority. A positive device record comes from a trusted registry. */\n async checkAuthority(\n person: FederationPrincipal,\n deviceKeyId: string,\n authority: FederationAuthority,\n ): Promise<FederationAuthority> {\n const result = queue.then(() => sync(person.issuer));\n queue = result.catch(() => undefined);\n let state;\n try {\n state = await result;\n } catch (error) {\n state = await ports.feed(person.issuer);\n if (\n state.syncedAt === undefined ||\n ports.now() < state.syncedAt ||\n ports.now() - state.syncedAt > FEDERATION_FRESHNESS.token\n )\n throw error;\n }\n const current = { ...authority };\n for (const row of state.records) {\n if (row.subject !== person.subject) continue;\n if (row.epoch !== null) current.revocationEpoch = Math.max(current.revocationEpoch, row.epoch);\n else if (row.deviceId === null) current.deviceRevoked = true;\n if (row.deviceId === deviceKeyId) current.deviceRevoked = true;\n }\n return current;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FederationAuthority, FederationIssuerPin, FederationPrincipal } from "../federationVerifier/type.mjs";
|
|
2
|
+
import { FederationIssuerHttpPorts } from "./type.mjs";
|
|
3
|
+
//#region src/services/federationIssuerHttp/index.d.ts
|
|
4
|
+
/** Real HTTP transport for the issuer's published routes. Redirects never escape a pin. */
|
|
5
|
+
export declare function createFederationIssuerHttp(ports: FederationIssuerHttpPorts): {
|
|
6
|
+
fetchJwks(issuer: string): Promise<FederationIssuerPin['jwks']>;
|
|
7
|
+
/** Host-authenticated key lookup, never a key supplied in an envelope. */
|
|
8
|
+
lookupDevice(person: FederationPrincipal, keyId: string): Promise<FederationAuthority>;
|
|
9
|
+
/** Feed only removes authority. A positive device record comes from a trusted registry. */
|
|
10
|
+
checkAuthority(person: FederationPrincipal, deviceKeyId: string, authority: FederationAuthority): Promise<FederationAuthority>;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/federationIssuerHttp/index.ts"],"mappings":";;;;wBAMgB,2BAA2B,OAAO;EAgDxC,UAAS,iBAAkB,QAAQ;;EAmBnC,aAAY,QAAS,qBAAmB,gBAAkB,QAAQ;;EA2BlE,eAAc,QACV,qBAAmB,qBACR,WACR,sBACV,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{FEDERATION_FRESHNESS as e}from"../../types/federation.mjs";import{isFederationIssuerOrigin as t}from"../federationVerifier/index.mjs";function n(n){async function r(e,r,i){if(!t(e)||(await n.issuer(e))?.issuer!==e)throw Error(`Unpinned issuer`);let a=await n.fetch(`${e}${r}`,{method:i===void 0?`GET`:`POST`,redirect:`manual`,cache:`no-store`,credentials:`omit`,headers:{accept:`application/json`,...i===void 0?{}:{"content-type":`application/json`}},...i===void 0?{}:{body:JSON.stringify(i)}});if(!a.ok||a.redirected||a.url&&new URL(a.url).origin!==e)throw Error(`Issuer request refused`);return a.json()}let i=Promise.resolve();async function a(e){let t=structuredClone(await n.feed(e));if(!Number.isSafeInteger(t.cursor)||t.cursor<0)throw Error(`Invalid feed cursor`);for(;;){let n=await r(e,`/api/auth/revocations?after=${t.cursor}`);if(!Array.isArray(n)||n.length>1e3)throw Error(`Invalid issuer feed`);for(let e of n){let n=e;if(!n||!Number.isSafeInteger(n.sequence)||n.sequence<=t.cursor||typeof n.subject!=`string`||!n.subject||n.deviceId!==null&&typeof n.deviceId!=`string`||!Number.isSafeInteger(n.createdAt)||n.createdAt<0||n.epoch!==null&&(!Number.isSafeInteger(n.epoch)||n.epoch<0))throw Error(`Invalid revocation row`);t.records.push(n),t.cursor=n.sequence}if(n.length<1e3)break}return t.syncedAt=n.now(),await n.saveFeed(e,t),t}return{async fetchJwks(e){let t=await r(e,`/api/auth/jwks`);if(!t||!Array.isArray(t.keys)||!t.keys.every(e=>e&&e.kty===`EC`&&e.crv===`P-256`&&typeof e.kid==`string`&&typeof e.x==`string`&&typeof e.y==`string`))throw Error(`Invalid issuer JWKS`);return t},async lookupDevice(e,t){if(!n.deviceKeyProof)throw Error(`Enrolled host proof required`);let i=await n.deviceKeyProof(e.issuer,t);if(i.payload.issuer!==e.issuer||i.payload.keyId!==t)throw Error(`Wrong key proof`);let a=await r(e.issuer,`/api/auth/device-key`,{proof:i});if(!a||a.issuer!==e.issuer||a.subject!==e.subject||a.keyId!==t||typeof a.publicKey!=`string`||typeof a.revoked!=`boolean`||![`tofu`,`approved`,`hub`,`recovered`,`org-admin`].includes(a.trust)||a.organizationId!==null&&typeof a.organizationId!=`string`||!Number.isSafeInteger(a.revocationEpoch)||a.revocationEpoch<0)throw Error(`Invalid device authority`);return{devicePublicKey:a.publicKey,deviceRevoked:a.revoked,revocationEpoch:a.revocationEpoch,trust:a.trust,organizationId:a.organizationId}},async checkAuthority(t,r,o){let s=i.then(()=>a(t.issuer));i=s.catch(()=>void 0);let c;try{c=await s}catch(r){if(c=await n.feed(t.issuer),c.syncedAt===void 0||n.now()<c.syncedAt||n.now()-c.syncedAt>e.token)throw r}let l={...o};for(let e of c.records)e.subject===t.subject&&(e.epoch===null?e.deviceId===null&&(l.deviceRevoked=!0):l.revocationEpoch=Math.max(l.revocationEpoch,e.epoch),e.deviceId===r&&(l.deviceRevoked=!0));return l}}}export{n as createFederationIssuerHttp};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["createFederationIssuerHttp","get","isFederationIssuerOrigin","issuer","ports","response","path","body","queue","sync","state","page","raw","row","value","key","proof","person","keyId","result","error","FEDERATION_FRESHNESS","current","authority","deviceKeyId"],"sources":["../../../src/services/federationIssuerHttp/index.ts"],"sourcesContent":["import { FEDERATION_FRESHNESS } from '../../types/federation';\nimport { isFederationIssuerOrigin } from '../federationVerifier';\nimport type { FederationAuthority, FederationIssuerPin, FederationPrincipal } from '../federationVerifier/type';\nimport type { FederationDeviceKeyResponse, FederationIssuerHttpPorts, FederationRevocation } from './type';\n\n/** Real HTTP transport for the issuer's published routes. Redirects never escape a pin. */\nexport function createFederationIssuerHttp(ports: FederationIssuerHttpPorts) {\n async function get(issuer: string, path: string, body?: unknown): Promise<unknown> {\n if (!isFederationIssuerOrigin(issuer) || (await ports.issuer(issuer))?.issuer !== issuer)\n throw new Error('Unpinned issuer');\n const response = await ports.fetch(`${issuer}${path}`, {\n method: body === undefined ? 'GET' : 'POST',\n redirect: 'manual',\n cache: 'no-store',\n credentials: 'omit',\n headers: { accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }) },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok || response.redirected || (response.url && new URL(response.url).origin !== issuer))\n throw new Error('Issuer request refused');\n return response.json();\n }\n let queue: Promise<unknown> = Promise.resolve();\n async function sync(issuer: string) {\n const state = structuredClone(await ports.feed(issuer));\n if (!Number.isSafeInteger(state.cursor) || state.cursor < 0) throw new Error('Invalid feed cursor');\n // Persist only after a complete fetch, never accept a partial or failed page.\n for (;;) {\n const page = await get(issuer, `/api/auth/revocations?after=${state.cursor}`);\n if (!Array.isArray(page) || page.length > 1000) throw new Error('Invalid issuer feed');\n for (const raw of page) {\n const row = raw as FederationRevocation;\n if (\n !row ||\n !Number.isSafeInteger(row.sequence) ||\n row.sequence <= state.cursor ||\n typeof row.subject !== 'string' ||\n !row.subject ||\n (row.deviceId !== null && typeof row.deviceId !== 'string') ||\n !Number.isSafeInteger(row.createdAt) ||\n row.createdAt < 0 ||\n (row.epoch !== null && (!Number.isSafeInteger(row.epoch) || row.epoch < 0))\n )\n throw new Error('Invalid revocation row');\n state.records.push(row);\n state.cursor = row.sequence;\n }\n if (page.length < 1000) break;\n }\n state.syncedAt = ports.now();\n await ports.saveFeed(issuer, state);\n return state;\n }\n return {\n async fetchJwks(issuer: string): Promise<FederationIssuerPin['jwks']> {\n const value = (await get(issuer, '/api/auth/jwks')) as FederationIssuerPin['jwks'];\n if (\n !value ||\n !Array.isArray(value.keys) ||\n !value.keys.every(\n (key) =>\n key &&\n key.kty === 'EC' &&\n key.crv === 'P-256' &&\n typeof key.kid === 'string' &&\n typeof key.x === 'string' &&\n typeof key.y === 'string',\n )\n )\n throw new Error('Invalid issuer JWKS');\n return value;\n },\n /** Host-authenticated key lookup, never a key supplied in an envelope. */\n async lookupDevice(person: FederationPrincipal, keyId: string): Promise<FederationAuthority> {\n if (!ports.deviceKeyProof) throw new Error('Enrolled host proof required');\n const proof = await ports.deviceKeyProof(person.issuer, keyId);\n if (proof.payload.issuer !== person.issuer || proof.payload.keyId !== keyId) throw new Error('Wrong key proof');\n const row = (await get(person.issuer, '/api/auth/device-key', { proof })) as FederationDeviceKeyResponse;\n if (\n !row ||\n row.issuer !== person.issuer ||\n row.subject !== person.subject ||\n row.keyId !== keyId ||\n typeof row.publicKey !== 'string' ||\n typeof row.revoked !== 'boolean' ||\n !['tofu', 'approved', 'hub', 'recovered', 'org-admin'].includes(row.trust) ||\n (row.organizationId !== null && typeof row.organizationId !== 'string') ||\n !Number.isSafeInteger(row.revocationEpoch) ||\n row.revocationEpoch < 0\n )\n throw new Error('Invalid device authority');\n return {\n devicePublicKey: row.publicKey,\n deviceRevoked: row.revoked,\n revocationEpoch: row.revocationEpoch,\n trust: row.trust,\n organizationId: row.organizationId,\n };\n },\n /** Feed only removes authority. A positive device record comes from a trusted registry. */\n async checkAuthority(\n person: FederationPrincipal,\n deviceKeyId: string,\n authority: FederationAuthority,\n ): Promise<FederationAuthority> {\n const result = queue.then(() => sync(person.issuer));\n queue = result.catch(() => undefined);\n let state;\n try {\n state = await result;\n } catch (error) {\n state = await ports.feed(person.issuer);\n if (\n state.syncedAt === undefined ||\n ports.now() < state.syncedAt ||\n ports.now() - state.syncedAt > FEDERATION_FRESHNESS.token\n )\n throw error;\n }\n const current = { ...authority };\n for (const row of state.records) {\n if (row.subject !== person.subject) continue;\n if (row.epoch !== null) current.revocationEpoch = Math.max(current.revocationEpoch, row.epoch);\n else if (row.deviceId === null) current.deviceRevoked = true;\n if (row.deviceId === deviceKeyId) current.deviceRevoked = true;\n }\n return current;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SignedFederationEnvelope } from "../../types/federation.mjs";
|
|
2
|
+
import { FederationIssuerPin } from "../federationVerifier/type.mjs";
|
|
3
|
+
//#region src/services/federationIssuerHttp/type.d.ts
|
|
4
|
+
export interface FederationDeviceKeyProof {
|
|
5
|
+
issuer: string;
|
|
6
|
+
hostId: string;
|
|
7
|
+
keyId: string;
|
|
8
|
+
issuedAt: number;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
nonce: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FederationDeviceKeyResponse {
|
|
13
|
+
keyId: string;
|
|
14
|
+
issuer: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
publicKey: string;
|
|
17
|
+
revoked: boolean;
|
|
18
|
+
revocationEpoch: number;
|
|
19
|
+
trust: 'tofu' | 'approved' | 'hub' | 'recovered' | 'org-admin';
|
|
20
|
+
organizationId: string | null;
|
|
21
|
+
}
|
|
22
|
+
/** Published /api/auth/revocations row, milliseconds except for the sequence. */
|
|
23
|
+
export interface FederationRevocation {
|
|
24
|
+
sequence: number;
|
|
25
|
+
subject: string;
|
|
26
|
+
deviceId: string | null;
|
|
27
|
+
createdAt: number;
|
|
28
|
+
epoch: number | null;
|
|
29
|
+
}
|
|
30
|
+
export interface FederationFeedState {
|
|
31
|
+
cursor: number;
|
|
32
|
+
records: FederationRevocation[];
|
|
33
|
+
syncedAt?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface FederationIssuerHttpPorts {
|
|
36
|
+
fetch: typeof fetch;
|
|
37
|
+
now(): number;
|
|
38
|
+
/** Host-enrolled key signs the lookup. Its nonce must be fresh for each call. */
|
|
39
|
+
deviceKeyProof?(issuer: string, keyId: string): Promise<SignedFederationEnvelope<FederationDeviceKeyProof, 'po/host-request/v1'>>;
|
|
40
|
+
/** Only locally pinned issuer origins can be contacted. */
|
|
41
|
+
issuer(issuer: string): Promise<FederationIssuerPin | undefined>;
|
|
42
|
+
feed(issuer: string): Promise<FederationFeedState>;
|
|
43
|
+
saveFeed(issuer: string, state: FederationFeedState): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//# sourceMappingURL=type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.d.mts","names":[],"sources":["../../../src/services/federationIssuerHttp/type.ts"],"mappings":";;;iBAGiB;EACf;EACA;EACA;EACA;EACA;EACA;;iBAEe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIe;EACf;EACA;EACA;EACA;EACA;;iBAEe;EACf;EACA,SAAS;EACT;;iBAEe;EACf,cAAc;EACd;;EAEA,gBACE,gBACA,gBACC,QAAQ,yBAAyB;;EAEpC,OAAO,iBAAiB,QAAQ;EAChC,KAAK,iBAAiB,QAAQ;EAC9B,SAAS,gBAAgB,OAAO,sBAAsB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../types/federation.cjs"),t=require("../federationEnvelope/index.cjs");let n=require("node:crypto");function r(e){return(0,n.createHash)(`sha256`).update(e).digest(`base64url`)}function i(e){let t=(0,n.createPublicKey)({key:Buffer.from(e,`base64url`),type:`spki`,format:`der`}).export({format:`jwk`});if(t.kty!==`EC`||t.crv!==`P-256`)throw Error(`Expected P-256 key`);return r(JSON.stringify({crv:t.crv,kty:t.kty,x:t.x,y:t.y}))}function a(e){return e===void 0?1:Number.isSafeInteger(e)&&e>0?e:0}function o(e){return e===void 0?0:Number.isSafeInteger(e)&&e>=0?e:-1}function s(e){try{let t=new URL(e);return t.protocol===`https:`&&t.origin===e}catch{return!1}}function c(c){let l=/* @__PURE__ */ new Map;async function u(t,r){let i=t.split(`.`);if(i.length!==3||i.some(e=>!/^[A-Za-z0-9_-]+$/.test(e)))return;let a=JSON.parse(Buffer.from(i[0],`base64url`).toString()),u=JSON.parse(Buffer.from(i[1],`base64url`).toString());if(a.alg!==`ES256`||!a.kid||a.crit!==void 0||!s(u.iss))return;let d=await c.issuer(u.iss);if(!d||d.issuer!==u.iss)return;let f=d.jwks.keys.some(e=>e.kid===a.kid),p=d.fetchedAt!==void 0&&Number.isSafeInteger(d.fetchedAt)&&d.fetchedAt>=0&&d.fetchedAt<=r&&r-d.fetchedAt<e.FEDERATION_FRESHNESS.jwks;if(!f||!p){let t=l.get(d.issuer);if(!f&&t!==void 0&&r-t<e.FEDERATION_FRESHNESS.jwksRefetch)return;l.set(d.issuer,r);let n=await c.fetchJwks(d.issuer);d={issuer:d.issuer,jwks:n,fetchedAt:r},await c.saveIssuer(d)}let m=d.jwks.keys.filter(e=>e.kid===a.kid),h=m[0];if(m.length!==1||h.kty!==`EC`||h.crv!==`P-256`||h.alg&&h.alg!==`ES256`||h.use&&h.use!==`sig`||!(0,n.verify)(`sha256`,Buffer.from(`${i[0]}.${i[1]}`),{key:(0,n.createPublicKey)({key:{...h},format:`jwk`}).export({format:`pem`,type:`spki`}),dsaEncoding:`ieee-p1363`},Buffer.from(i[2],`base64url`)))return;let g=u.act!==void 0||u.grant!==void 0;if(!(![u.sub,u.org,u.aud,u.jti,u.cnf?.jkt].every(e=>typeof e==`string`&&e.length>0)||![`owner`,`admin`,`member`].includes(u.org_role)||!Number.isSafeInteger(u.iat)||!Number.isSafeInteger(u.exp)||u.iat<0||u.exp<=u.iat||u.iat*1e3>r+e.FEDERATION_FRESHNESS.clockSkew||r>=u.exp*1e3-e.FEDERATION_FRESHNESS.clockSkew||u.nbf!==void 0&&(!Number.isSafeInteger(u.nbf)||u.nbf<0||u.nbf*1e3>r+e.FEDERATION_FRESHNESS.clockSkew)||(u.exp-u.iat)*1e3>(g?e.FEDERATION_FRESHNESS.delegatedToken:e.FEDERATION_FRESHNESS.token)||o(u.revocation_epoch)<0||g&&(!u.act?.hostId||!u.grant)))return u}return{async verify(n){try{let s=c.now();if(!Number.isSafeInteger(s)||s<0)return;let l=await u(n.token,s),d=n.envelope,f=d?.payload;if(!l||!f||typeof f.deviceKeyId!=`string`||!f.deviceKeyId||l.aud!==n.networkId||l.org!==n.org||f.issuer!==l.iss||f.subject!==l.sub||f.destinationHostId!==c.hostId||f.networkId!==n.networkId||f.workspaceId!==n.workspaceId||f.sessionId!==n.sessionId||f.method!==n.method||f.canonicalTarget!==n.canonicalTarget||f.bodyDigest!==r(n.body)||f.tokenHash!==r(n.token)||typeof f.nonce!=`string`||!f.nonce||f.nonce.length>256)return;let p={issuer:l.iss,subject:l.sub},m=await c.authority(p,l.org,f.deviceKeyId);if(!m||m.deviceRevoked||o(m.revocationEpoch)<0||o(l.revocation_epoch)!==m.revocationEpoch)return;let h={principal:p,org:l.org,device:{keyId:f.deviceKeyId}},g,_=async()=>!0;if(l.act){if(d.tag!==`po/host-request/v1`||f.callingHostId!==l.act.hostId||f.grantId!==l.grant||f.delegatedTokenHash!==r(n.token)||!f.actingPrincipal?.issuer||!f.actingPrincipal.subject)return;let e=await c.hostKey(l.act.hostId,n.networkId);if(!e||i(e)!==l.cnf.jkt||!t.verifyEnvelope(e,`po/host-request/v1`,d,s))return;let u=n.grant;if(g=u?.payload,!u||!g||!t.verifyEnvelope(m.devicePublicKey,`po/grant/v1`,u,s)||g.issuer!==l.iss||g.subject!==l.sub||g.deviceKeyId!==f.deviceKeyId||g.grantId!==l.grant||g.callingHostId!==l.act.hostId||g.targetHostId!==c.hostId||g.networkId!==n.networkId||g.workspaceId!==n.workspaceId||g.redelegable!==!1||o(g.revocationEpoch)!==m.revocationEpoch||!Array.isArray(g.operations)||!g.operations.includes(n.operation)||!g.constraints||typeof g.constraints!=`object`||Array.isArray(g.constraints)||!Object.values(g.constraints).every(e=>e===null||typeof e==`string`||typeof e==`boolean`||typeof e==`number`&&Number.isFinite(e))||!Number.isSafeInteger(g.notBefore)||s<g.notBefore)return;let p=l.act.hostId,v=a(g.callingEnrollmentGeneration),y=a(g.targetEnrollmentGeneration);if(!v||!y||a(l.act.enrollmentGeneration)!==v||(_=async()=>{let t=c.hostEnrollmentGeneration?await c.hostEnrollmentGeneration(p,n.networkId):1,r=c.hostEnrollmentGeneration?await c.hostEnrollmentGeneration(c.hostId,n.networkId):1;return t===v&&r===y&&await c.hostKey(p,n.networkId)===e},!await _()))return;h.via={callingHostId:l.act.hostId,grantId:l.grant}}else if(i(m.devicePublicKey)!==l.cnf.jkt||d.tag!==`po/request/v1`||f.callingHostId!==void 0||f.grantId!==void 0||!t.verifyEnvelope(m.devicePublicKey,`po/request/v1`,d,s))return;if(!await c.authorize(h,n,g)||!await _())return;let v=Math.min(f.expiresAt,l.exp*1e3,g?.expiresAt??1/0)-e.FEDERATION_FRESHNESS.clockSkew,y=c.now();if(!Number.isSafeInteger(y)||y<s||y>=v)return;let b=JSON.stringify([c.hostId,n.networkId,p.issuer,p.subject,h.via?.callingHostId??f.deviceKeyId]);if(!await c.replay.admit(b,f.nonce,f.expiresAt,y))return;let x=c.now();return Number.isSafeInteger(x)&&x>=y&&x<v&&await _()?h:void 0}catch{return}}}}exports.createFederationVerifier=c,exports.federationDigest=r,exports.federationKeyThumbprint=i,exports.isFederationIssuerOrigin=s;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["createHash","createPublicKey","FEDERATION_FRESHNESS","verify","verifyEnvelope","require_federation","require_index","node_crypto","federationDigest","value","federationKeyThumbprint","key","spki","enrollment","epoch","isFederationIssuerOrigin","url","issuer","createFederationVerifier","lastFetch","tokenClaims","parts","token","part","header","claims","pin","ports","known","fresh","now","previous","jwks","matches","delegated","input","envelope","p","principal","authority","caller","grant","endpointsCurrent","signedGrant","callingHostId","callingGeneration","targetGeneration","calling","target","deadline","admittedAt","scope","completedAt"],"sources":["../../../src/services/federationVerifier/index.ts"],"sourcesContent":["import { createHash, createPublicKey, verify } from 'node:crypto';\n\nimport { FEDERATION_FRESHNESS } from '../../types/federation';\nimport { verifyEnvelope } from '../federationEnvelope';\nimport type {\n FederationCaller,\n FederationConsentGrant,\n FederationTokenClaims,\n FederationVerifierPorts,\n FederationVerifyInput,\n} from './type';\n\nexport function federationDigest(value: string): string {\n return createHash('sha256').update(value).digest('base64url');\n}\n\nexport function federationKeyThumbprint(spki: string): string {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), type: 'spki', format: 'der' }).export({\n format: 'jwk',\n });\n if (key.kty !== 'EC' || key.crv !== 'P-256') throw new Error('Expected P-256 key');\n return federationDigest(JSON.stringify({ crv: key.crv, kty: key.kty, x: key.x, y: key.y }));\n}\n\nfunction enrollment(value: number | undefined): number {\n return value === undefined ? 1 : Number.isSafeInteger(value) && value > 0 ? value : 0;\n}\n\nfunction epoch(value: number | undefined): number {\n return value === undefined ? 0 : Number.isSafeInteger(value) && value >= 0 ? value : -1;\n}\n\n/** Only exact HTTPS origins may be pinned. Token headers never choose a URL. */\nexport function isFederationIssuerOrigin(issuer: string): boolean {\n try {\n const url = new URL(issuer);\n return url.protocol === 'https:' && url.origin === issuer;\n } catch {\n return false;\n }\n}\n\nexport function createFederationVerifier(ports: FederationVerifierPorts) {\n // Pinned issuers only. Failed fetches also consume the unknown-kid retry window.\n // ponytail: retry state is process-local; persist it if restart-driven fetch abuse becomes material.\n const lastFetch = new Map<string, number>();\n async function tokenClaims(token: string, now: number): Promise<FederationTokenClaims | undefined> {\n const parts = token.split('.');\n if (parts.length !== 3 || parts.some((part) => !/^[A-Za-z0-9_-]+$/.test(part))) return undefined;\n const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString()) as {\n alg?: string;\n kid?: string;\n crit?: unknown;\n };\n const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString()) as FederationTokenClaims;\n if (header.alg !== 'ES256' || !header.kid || header.crit !== undefined || !isFederationIssuerOrigin(claims.iss))\n return undefined;\n let pin = await ports.issuer(claims.iss);\n if (!pin || pin.issuer !== claims.iss) return undefined;\n const known = pin.jwks.keys.some((key) => key.kid === header.kid);\n const fresh =\n pin.fetchedAt !== undefined &&\n Number.isSafeInteger(pin.fetchedAt) &&\n pin.fetchedAt >= 0 &&\n pin.fetchedAt <= now &&\n now - pin.fetchedAt < FEDERATION_FRESHNESS.jwks;\n if (!known || !fresh) {\n const previous = lastFetch.get(pin.issuer);\n if (!known && previous !== undefined && now - previous < FEDERATION_FRESHNESS.jwksRefetch) return undefined;\n lastFetch.set(pin.issuer, now);\n const jwks = await ports.fetchJwks(pin.issuer);\n // Replace, never merge: a removed key loses authority at this refetch.\n pin = { issuer: pin.issuer, jwks, fetchedAt: now };\n await ports.saveIssuer(pin);\n }\n const matches = pin.jwks.keys.filter((key) => key.kid === header.kid);\n const key = matches[0];\n if (\n matches.length !== 1 ||\n key.kty !== 'EC' ||\n key.crv !== 'P-256' ||\n (key.alg && key.alg !== 'ES256') ||\n (key.use && key.use !== 'sig')\n )\n return undefined;\n if (\n !verify(\n 'sha256',\n Buffer.from(`${parts[0]}.${parts[1]}`),\n {\n key: createPublicKey({ key: { ...key }, format: 'jwk' }).export({ format: 'pem', type: 'spki' }),\n dsaEncoding: 'ieee-p1363',\n },\n Buffer.from(parts[2], 'base64url'),\n )\n )\n return undefined;\n const delegated = claims.act !== undefined || claims.grant !== undefined;\n if (\n ![claims.sub, claims.org, claims.aud, claims.jti, claims.cnf?.jkt].every(\n (value) => typeof value === 'string' && value.length > 0,\n ) ||\n !['owner', 'admin', 'member'].includes(claims.org_role) ||\n !Number.isSafeInteger(claims.iat) ||\n !Number.isSafeInteger(claims.exp) ||\n claims.iat < 0 ||\n claims.exp <= claims.iat ||\n claims.iat * 1000 > now + FEDERATION_FRESHNESS.clockSkew ||\n now >= claims.exp * 1000 - FEDERATION_FRESHNESS.clockSkew ||\n (claims.nbf !== undefined &&\n (!Number.isSafeInteger(claims.nbf) ||\n claims.nbf < 0 ||\n claims.nbf * 1000 > now + FEDERATION_FRESHNESS.clockSkew)) ||\n (claims.exp - claims.iat) * 1000 >\n (delegated ? FEDERATION_FRESHNESS.delegatedToken : FEDERATION_FRESHNESS.token) ||\n epoch(claims.revocation_epoch) < 0 ||\n (delegated && (!claims.act?.hostId || !claims.grant))\n )\n return undefined;\n return claims;\n }\n return {\n /** All failures, including unavailable authority/replay storage, refuse admission. */\n async verify(input: FederationVerifyInput): Promise<FederationCaller | undefined> {\n try {\n const now = ports.now();\n if (!Number.isSafeInteger(now) || now < 0) return undefined;\n const token = await tokenClaims(input.token, now);\n const envelope = input.envelope;\n const p = envelope?.payload;\n if (\n !token ||\n !p ||\n typeof p.deviceKeyId !== 'string' ||\n !p.deviceKeyId ||\n token.aud !== input.networkId ||\n token.org !== input.org ||\n p.issuer !== token.iss ||\n p.subject !== token.sub ||\n p.destinationHostId !== ports.hostId ||\n p.networkId !== input.networkId ||\n p.workspaceId !== input.workspaceId ||\n p.sessionId !== input.sessionId ||\n p.method !== input.method ||\n p.canonicalTarget !== input.canonicalTarget ||\n p.bodyDigest !== federationDigest(input.body) ||\n p.tokenHash !== federationDigest(input.token) ||\n typeof p.nonce !== 'string' ||\n !p.nonce ||\n p.nonce.length > 256\n )\n return undefined;\n const principal = { issuer: token.iss, subject: token.sub };\n const authority = await ports.authority(principal, token.org, p.deviceKeyId);\n if (\n !authority ||\n authority.deviceRevoked ||\n epoch(authority.revocationEpoch) < 0 ||\n epoch(token.revocation_epoch) !== authority.revocationEpoch\n )\n return undefined;\n const caller: FederationCaller = { principal, org: token.org, device: { keyId: p.deviceKeyId } };\n let grant: FederationConsentGrant | undefined;\n let endpointsCurrent = async () => true;\n if (token.act) {\n if (\n envelope.tag !== 'po/host-request/v1' ||\n p.callingHostId !== token.act.hostId ||\n p.grantId !== token.grant ||\n p.delegatedTokenHash !== federationDigest(input.token) ||\n !p.actingPrincipal?.issuer ||\n !p.actingPrincipal.subject\n )\n return undefined;\n const key = await ports.hostKey(token.act.hostId, input.networkId);\n if (\n !key ||\n federationKeyThumbprint(key) !== token.cnf.jkt ||\n !verifyEnvelope(key, 'po/host-request/v1', envelope, now)\n )\n return undefined;\n const signedGrant = input.grant;\n grant = signedGrant?.payload;\n if (\n !signedGrant ||\n !grant ||\n !verifyEnvelope(authority.devicePublicKey, 'po/grant/v1', signedGrant, now) ||\n grant.issuer !== token.iss ||\n grant.subject !== token.sub ||\n grant.deviceKeyId !== p.deviceKeyId ||\n grant.grantId !== token.grant ||\n grant.callingHostId !== token.act.hostId ||\n grant.targetHostId !== ports.hostId ||\n grant.networkId !== input.networkId ||\n grant.workspaceId !== input.workspaceId ||\n grant.redelegable !== false ||\n epoch(grant.revocationEpoch) !== authority.revocationEpoch ||\n !Array.isArray(grant.operations) ||\n !grant.operations.includes(input.operation) ||\n !grant.constraints ||\n typeof grant.constraints !== 'object' ||\n Array.isArray(grant.constraints) ||\n !Object.values(grant.constraints).every(\n (value) =>\n value === null ||\n typeof value === 'string' ||\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value)),\n ) ||\n !Number.isSafeInteger(grant.notBefore) ||\n now < grant.notBefore\n )\n return undefined;\n const callingHostId = token.act.hostId;\n const callingGeneration = enrollment(grant.callingEnrollmentGeneration);\n const targetGeneration = enrollment(grant.targetEnrollmentGeneration);\n if (\n !callingGeneration ||\n !targetGeneration ||\n enrollment(token.act.enrollmentGeneration) !== callingGeneration\n )\n return undefined;\n endpointsCurrent = async () => {\n const calling = ports.hostEnrollmentGeneration\n ? await ports.hostEnrollmentGeneration(callingHostId, input.networkId)\n : 1;\n const target = ports.hostEnrollmentGeneration\n ? await ports.hostEnrollmentGeneration(ports.hostId, input.networkId)\n : 1;\n return (\n calling === callingGeneration &&\n target === targetGeneration &&\n (await ports.hostKey(callingHostId, input.networkId)) === key\n );\n };\n if (!(await endpointsCurrent())) return undefined;\n caller.via = { callingHostId: token.act.hostId, grantId: token.grant! };\n } else {\n if (\n federationKeyThumbprint(authority.devicePublicKey) !== token.cnf.jkt ||\n envelope.tag !== 'po/request/v1' ||\n p.callingHostId !== undefined ||\n p.grantId !== undefined ||\n !verifyEnvelope(authority.devicePublicKey, 'po/request/v1', envelope, now)\n )\n return undefined;\n }\n if (!(await ports.authorize(caller, input, grant)) || !(await endpointsCurrent())) return undefined;\n const deadline =\n Math.min(p.expiresAt, token.exp * 1000, grant?.expiresAt ?? Infinity) - FEDERATION_FRESHNESS.clockSkew;\n const admittedAt = ports.now();\n if (!Number.isSafeInteger(admittedAt) || admittedAt < now || admittedAt >= deadline) return undefined;\n const scope = JSON.stringify([\n ports.hostId,\n input.networkId,\n principal.issuer,\n principal.subject,\n caller.via?.callingHostId ?? p.deviceKeyId,\n ]);\n if (!(await ports.replay.admit(scope, p.nonce, p.expiresAt, admittedAt))) return undefined;\n const completedAt = ports.now();\n return Number.isSafeInteger(completedAt) &&\n completedAt >= admittedAt &&\n completedAt < deadline &&\n (await endpointsCurrent())\n ? caller\n : undefined;\n } catch {\n return undefined;\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FederationCaller, FederationVerifierPorts, FederationVerifyInput } from "./type.mjs";
|
|
2
|
+
//#region src/services/federationVerifier/index.d.ts
|
|
3
|
+
export declare function federationDigest(value: string): string;
|
|
4
|
+
export declare function federationKeyThumbprint(spki: string): string;
|
|
5
|
+
/** Only exact HTTPS origins may be pinned. Token headers never choose a URL. */
|
|
6
|
+
export declare function isFederationIssuerOrigin(issuer: string): boolean;
|
|
7
|
+
export declare function createFederationVerifier(ports: FederationVerifierPorts): {
|
|
8
|
+
/** All failures, including unavailable authority/replay storage, refuse admission. */
|
|
9
|
+
verify(input: FederationVerifyInput): Promise<FederationCaller | undefined>;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/federationVerifier/index.ts"],"mappings":";;wBAYgB,iBAAiB;wBAIjB,wBAAwB;;wBAiBxB,yBAAyB;wBASzB,yBAAyB,OAAO;;EAiFtC,OAAM,OAAQ,wBAAwB,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{FEDERATION_FRESHNESS as e}from"../../types/federation.mjs";import{verifyEnvelope as t}from"../federationEnvelope/index.mjs";import{createHash as n,createPublicKey as r,verify as i}from"node:crypto";function a(e){return n(`sha256`).update(e).digest(`base64url`)}function o(e){let t=r({key:Buffer.from(e,`base64url`),type:`spki`,format:`der`}).export({format:`jwk`});if(t.kty!==`EC`||t.crv!==`P-256`)throw Error(`Expected P-256 key`);return a(JSON.stringify({crv:t.crv,kty:t.kty,x:t.x,y:t.y}))}function s(e){return e===void 0?1:Number.isSafeInteger(e)&&e>0?e:0}function c(e){return e===void 0?0:Number.isSafeInteger(e)&&e>=0?e:-1}function l(e){try{let t=new URL(e);return t.protocol===`https:`&&t.origin===e}catch{return!1}}function u(n){let u=/* @__PURE__ */ new Map;async function d(t,a){let o=t.split(`.`);if(o.length!==3||o.some(e=>!/^[A-Za-z0-9_-]+$/.test(e)))return;let s=JSON.parse(Buffer.from(o[0],`base64url`).toString()),d=JSON.parse(Buffer.from(o[1],`base64url`).toString());if(s.alg!==`ES256`||!s.kid||s.crit!==void 0||!l(d.iss))return;let f=await n.issuer(d.iss);if(!f||f.issuer!==d.iss)return;let p=f.jwks.keys.some(e=>e.kid===s.kid),m=f.fetchedAt!==void 0&&Number.isSafeInteger(f.fetchedAt)&&f.fetchedAt>=0&&f.fetchedAt<=a&&a-f.fetchedAt<e.jwks;if(!p||!m){let t=u.get(f.issuer);if(!p&&t!==void 0&&a-t<e.jwksRefetch)return;u.set(f.issuer,a);let r=await n.fetchJwks(f.issuer);f={issuer:f.issuer,jwks:r,fetchedAt:a},await n.saveIssuer(f)}let h=f.jwks.keys.filter(e=>e.kid===s.kid),g=h[0];if(h.length!==1||g.kty!==`EC`||g.crv!==`P-256`||g.alg&&g.alg!==`ES256`||g.use&&g.use!==`sig`||!i(`sha256`,Buffer.from(`${o[0]}.${o[1]}`),{key:r({key:{...g},format:`jwk`}).export({format:`pem`,type:`spki`}),dsaEncoding:`ieee-p1363`},Buffer.from(o[2],`base64url`)))return;let _=d.act!==void 0||d.grant!==void 0;if(!(![d.sub,d.org,d.aud,d.jti,d.cnf?.jkt].every(e=>typeof e==`string`&&e.length>0)||![`owner`,`admin`,`member`].includes(d.org_role)||!Number.isSafeInteger(d.iat)||!Number.isSafeInteger(d.exp)||d.iat<0||d.exp<=d.iat||d.iat*1e3>a+e.clockSkew||a>=d.exp*1e3-e.clockSkew||d.nbf!==void 0&&(!Number.isSafeInteger(d.nbf)||d.nbf<0||d.nbf*1e3>a+e.clockSkew)||(d.exp-d.iat)*1e3>(_?e.delegatedToken:e.token)||c(d.revocation_epoch)<0||_&&(!d.act?.hostId||!d.grant)))return d}return{async verify(r){try{let i=n.now();if(!Number.isSafeInteger(i)||i<0)return;let l=await d(r.token,i),u=r.envelope,f=u?.payload;if(!l||!f||typeof f.deviceKeyId!=`string`||!f.deviceKeyId||l.aud!==r.networkId||l.org!==r.org||f.issuer!==l.iss||f.subject!==l.sub||f.destinationHostId!==n.hostId||f.networkId!==r.networkId||f.workspaceId!==r.workspaceId||f.sessionId!==r.sessionId||f.method!==r.method||f.canonicalTarget!==r.canonicalTarget||f.bodyDigest!==a(r.body)||f.tokenHash!==a(r.token)||typeof f.nonce!=`string`||!f.nonce||f.nonce.length>256)return;let p={issuer:l.iss,subject:l.sub},m=await n.authority(p,l.org,f.deviceKeyId);if(!m||m.deviceRevoked||c(m.revocationEpoch)<0||c(l.revocation_epoch)!==m.revocationEpoch)return;let h={principal:p,org:l.org,device:{keyId:f.deviceKeyId}},g,_=async()=>!0;if(l.act){if(u.tag!==`po/host-request/v1`||f.callingHostId!==l.act.hostId||f.grantId!==l.grant||f.delegatedTokenHash!==a(r.token)||!f.actingPrincipal?.issuer||!f.actingPrincipal.subject)return;let e=await n.hostKey(l.act.hostId,r.networkId);if(!e||o(e)!==l.cnf.jkt||!t(e,`po/host-request/v1`,u,i))return;let d=r.grant;if(g=d?.payload,!d||!g||!t(m.devicePublicKey,`po/grant/v1`,d,i)||g.issuer!==l.iss||g.subject!==l.sub||g.deviceKeyId!==f.deviceKeyId||g.grantId!==l.grant||g.callingHostId!==l.act.hostId||g.targetHostId!==n.hostId||g.networkId!==r.networkId||g.workspaceId!==r.workspaceId||g.redelegable!==!1||c(g.revocationEpoch)!==m.revocationEpoch||!Array.isArray(g.operations)||!g.operations.includes(r.operation)||!g.constraints||typeof g.constraints!=`object`||Array.isArray(g.constraints)||!Object.values(g.constraints).every(e=>e===null||typeof e==`string`||typeof e==`boolean`||typeof e==`number`&&Number.isFinite(e))||!Number.isSafeInteger(g.notBefore)||i<g.notBefore)return;let p=l.act.hostId,v=s(g.callingEnrollmentGeneration),y=s(g.targetEnrollmentGeneration);if(!v||!y||s(l.act.enrollmentGeneration)!==v||(_=async()=>{let t=n.hostEnrollmentGeneration?await n.hostEnrollmentGeneration(p,r.networkId):1,i=n.hostEnrollmentGeneration?await n.hostEnrollmentGeneration(n.hostId,r.networkId):1;return t===v&&i===y&&await n.hostKey(p,r.networkId)===e},!await _()))return;h.via={callingHostId:l.act.hostId,grantId:l.grant}}else if(o(m.devicePublicKey)!==l.cnf.jkt||u.tag!==`po/request/v1`||f.callingHostId!==void 0||f.grantId!==void 0||!t(m.devicePublicKey,`po/request/v1`,u,i))return;if(!await n.authorize(h,r,g)||!await _())return;let v=Math.min(f.expiresAt,l.exp*1e3,g?.expiresAt??1/0)-e.clockSkew,y=n.now();if(!Number.isSafeInteger(y)||y<i||y>=v)return;let b=JSON.stringify([n.hostId,r.networkId,p.issuer,p.subject,h.via?.callingHostId??f.deviceKeyId]);if(!await n.replay.admit(b,f.nonce,f.expiresAt,y))return;let x=n.now();return Number.isSafeInteger(x)&&x>=y&&x<v&&await _()?h:void 0}catch{return}}}}export{u as createFederationVerifier,a as federationDigest,o as federationKeyThumbprint,l as isFederationIssuerOrigin};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["federationDigest","createHash","value","federationKeyThumbprint","key","createPublicKey","spki","enrollment","epoch","isFederationIssuerOrigin","url","issuer","createFederationVerifier","lastFetch","tokenClaims","parts","token","part","header","claims","pin","ports","known","fresh","now","FEDERATION_FRESHNESS","previous","jwks","matches","verify","delegated","input","envelope","p","principal","authority","caller","grant","endpointsCurrent","verifyEnvelope","signedGrant","callingHostId","callingGeneration","targetGeneration","calling","target","deadline","admittedAt","scope","completedAt"],"sources":["../../../src/services/federationVerifier/index.ts"],"sourcesContent":["import { createHash, createPublicKey, verify } from 'node:crypto';\n\nimport { FEDERATION_FRESHNESS } from '../../types/federation';\nimport { verifyEnvelope } from '../federationEnvelope';\nimport type {\n FederationCaller,\n FederationConsentGrant,\n FederationTokenClaims,\n FederationVerifierPorts,\n FederationVerifyInput,\n} from './type';\n\nexport function federationDigest(value: string): string {\n return createHash('sha256').update(value).digest('base64url');\n}\n\nexport function federationKeyThumbprint(spki: string): string {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), type: 'spki', format: 'der' }).export({\n format: 'jwk',\n });\n if (key.kty !== 'EC' || key.crv !== 'P-256') throw new Error('Expected P-256 key');\n return federationDigest(JSON.stringify({ crv: key.crv, kty: key.kty, x: key.x, y: key.y }));\n}\n\nfunction enrollment(value: number | undefined): number {\n return value === undefined ? 1 : Number.isSafeInteger(value) && value > 0 ? value : 0;\n}\n\nfunction epoch(value: number | undefined): number {\n return value === undefined ? 0 : Number.isSafeInteger(value) && value >= 0 ? value : -1;\n}\n\n/** Only exact HTTPS origins may be pinned. Token headers never choose a URL. */\nexport function isFederationIssuerOrigin(issuer: string): boolean {\n try {\n const url = new URL(issuer);\n return url.protocol === 'https:' && url.origin === issuer;\n } catch {\n return false;\n }\n}\n\nexport function createFederationVerifier(ports: FederationVerifierPorts) {\n // Pinned issuers only. Failed fetches also consume the unknown-kid retry window.\n // ponytail: retry state is process-local; persist it if restart-driven fetch abuse becomes material.\n const lastFetch = new Map<string, number>();\n async function tokenClaims(token: string, now: number): Promise<FederationTokenClaims | undefined> {\n const parts = token.split('.');\n if (parts.length !== 3 || parts.some((part) => !/^[A-Za-z0-9_-]+$/.test(part))) return undefined;\n const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString()) as {\n alg?: string;\n kid?: string;\n crit?: unknown;\n };\n const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString()) as FederationTokenClaims;\n if (header.alg !== 'ES256' || !header.kid || header.crit !== undefined || !isFederationIssuerOrigin(claims.iss))\n return undefined;\n let pin = await ports.issuer(claims.iss);\n if (!pin || pin.issuer !== claims.iss) return undefined;\n const known = pin.jwks.keys.some((key) => key.kid === header.kid);\n const fresh =\n pin.fetchedAt !== undefined &&\n Number.isSafeInteger(pin.fetchedAt) &&\n pin.fetchedAt >= 0 &&\n pin.fetchedAt <= now &&\n now - pin.fetchedAt < FEDERATION_FRESHNESS.jwks;\n if (!known || !fresh) {\n const previous = lastFetch.get(pin.issuer);\n if (!known && previous !== undefined && now - previous < FEDERATION_FRESHNESS.jwksRefetch) return undefined;\n lastFetch.set(pin.issuer, now);\n const jwks = await ports.fetchJwks(pin.issuer);\n // Replace, never merge: a removed key loses authority at this refetch.\n pin = { issuer: pin.issuer, jwks, fetchedAt: now };\n await ports.saveIssuer(pin);\n }\n const matches = pin.jwks.keys.filter((key) => key.kid === header.kid);\n const key = matches[0];\n if (\n matches.length !== 1 ||\n key.kty !== 'EC' ||\n key.crv !== 'P-256' ||\n (key.alg && key.alg !== 'ES256') ||\n (key.use && key.use !== 'sig')\n )\n return undefined;\n if (\n !verify(\n 'sha256',\n Buffer.from(`${parts[0]}.${parts[1]}`),\n {\n key: createPublicKey({ key: { ...key }, format: 'jwk' }).export({ format: 'pem', type: 'spki' }),\n dsaEncoding: 'ieee-p1363',\n },\n Buffer.from(parts[2], 'base64url'),\n )\n )\n return undefined;\n const delegated = claims.act !== undefined || claims.grant !== undefined;\n if (\n ![claims.sub, claims.org, claims.aud, claims.jti, claims.cnf?.jkt].every(\n (value) => typeof value === 'string' && value.length > 0,\n ) ||\n !['owner', 'admin', 'member'].includes(claims.org_role) ||\n !Number.isSafeInteger(claims.iat) ||\n !Number.isSafeInteger(claims.exp) ||\n claims.iat < 0 ||\n claims.exp <= claims.iat ||\n claims.iat * 1000 > now + FEDERATION_FRESHNESS.clockSkew ||\n now >= claims.exp * 1000 - FEDERATION_FRESHNESS.clockSkew ||\n (claims.nbf !== undefined &&\n (!Number.isSafeInteger(claims.nbf) ||\n claims.nbf < 0 ||\n claims.nbf * 1000 > now + FEDERATION_FRESHNESS.clockSkew)) ||\n (claims.exp - claims.iat) * 1000 >\n (delegated ? FEDERATION_FRESHNESS.delegatedToken : FEDERATION_FRESHNESS.token) ||\n epoch(claims.revocation_epoch) < 0 ||\n (delegated && (!claims.act?.hostId || !claims.grant))\n )\n return undefined;\n return claims;\n }\n return {\n /** All failures, including unavailable authority/replay storage, refuse admission. */\n async verify(input: FederationVerifyInput): Promise<FederationCaller | undefined> {\n try {\n const now = ports.now();\n if (!Number.isSafeInteger(now) || now < 0) return undefined;\n const token = await tokenClaims(input.token, now);\n const envelope = input.envelope;\n const p = envelope?.payload;\n if (\n !token ||\n !p ||\n typeof p.deviceKeyId !== 'string' ||\n !p.deviceKeyId ||\n token.aud !== input.networkId ||\n token.org !== input.org ||\n p.issuer !== token.iss ||\n p.subject !== token.sub ||\n p.destinationHostId !== ports.hostId ||\n p.networkId !== input.networkId ||\n p.workspaceId !== input.workspaceId ||\n p.sessionId !== input.sessionId ||\n p.method !== input.method ||\n p.canonicalTarget !== input.canonicalTarget ||\n p.bodyDigest !== federationDigest(input.body) ||\n p.tokenHash !== federationDigest(input.token) ||\n typeof p.nonce !== 'string' ||\n !p.nonce ||\n p.nonce.length > 256\n )\n return undefined;\n const principal = { issuer: token.iss, subject: token.sub };\n const authority = await ports.authority(principal, token.org, p.deviceKeyId);\n if (\n !authority ||\n authority.deviceRevoked ||\n epoch(authority.revocationEpoch) < 0 ||\n epoch(token.revocation_epoch) !== authority.revocationEpoch\n )\n return undefined;\n const caller: FederationCaller = { principal, org: token.org, device: { keyId: p.deviceKeyId } };\n let grant: FederationConsentGrant | undefined;\n let endpointsCurrent = async () => true;\n if (token.act) {\n if (\n envelope.tag !== 'po/host-request/v1' ||\n p.callingHostId !== token.act.hostId ||\n p.grantId !== token.grant ||\n p.delegatedTokenHash !== federationDigest(input.token) ||\n !p.actingPrincipal?.issuer ||\n !p.actingPrincipal.subject\n )\n return undefined;\n const key = await ports.hostKey(token.act.hostId, input.networkId);\n if (\n !key ||\n federationKeyThumbprint(key) !== token.cnf.jkt ||\n !verifyEnvelope(key, 'po/host-request/v1', envelope, now)\n )\n return undefined;\n const signedGrant = input.grant;\n grant = signedGrant?.payload;\n if (\n !signedGrant ||\n !grant ||\n !verifyEnvelope(authority.devicePublicKey, 'po/grant/v1', signedGrant, now) ||\n grant.issuer !== token.iss ||\n grant.subject !== token.sub ||\n grant.deviceKeyId !== p.deviceKeyId ||\n grant.grantId !== token.grant ||\n grant.callingHostId !== token.act.hostId ||\n grant.targetHostId !== ports.hostId ||\n grant.networkId !== input.networkId ||\n grant.workspaceId !== input.workspaceId ||\n grant.redelegable !== false ||\n epoch(grant.revocationEpoch) !== authority.revocationEpoch ||\n !Array.isArray(grant.operations) ||\n !grant.operations.includes(input.operation) ||\n !grant.constraints ||\n typeof grant.constraints !== 'object' ||\n Array.isArray(grant.constraints) ||\n !Object.values(grant.constraints).every(\n (value) =>\n value === null ||\n typeof value === 'string' ||\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value)),\n ) ||\n !Number.isSafeInteger(grant.notBefore) ||\n now < grant.notBefore\n )\n return undefined;\n const callingHostId = token.act.hostId;\n const callingGeneration = enrollment(grant.callingEnrollmentGeneration);\n const targetGeneration = enrollment(grant.targetEnrollmentGeneration);\n if (\n !callingGeneration ||\n !targetGeneration ||\n enrollment(token.act.enrollmentGeneration) !== callingGeneration\n )\n return undefined;\n endpointsCurrent = async () => {\n const calling = ports.hostEnrollmentGeneration\n ? await ports.hostEnrollmentGeneration(callingHostId, input.networkId)\n : 1;\n const target = ports.hostEnrollmentGeneration\n ? await ports.hostEnrollmentGeneration(ports.hostId, input.networkId)\n : 1;\n return (\n calling === callingGeneration &&\n target === targetGeneration &&\n (await ports.hostKey(callingHostId, input.networkId)) === key\n );\n };\n if (!(await endpointsCurrent())) return undefined;\n caller.via = { callingHostId: token.act.hostId, grantId: token.grant! };\n } else {\n if (\n federationKeyThumbprint(authority.devicePublicKey) !== token.cnf.jkt ||\n envelope.tag !== 'po/request/v1' ||\n p.callingHostId !== undefined ||\n p.grantId !== undefined ||\n !verifyEnvelope(authority.devicePublicKey, 'po/request/v1', envelope, now)\n )\n return undefined;\n }\n if (!(await ports.authorize(caller, input, grant)) || !(await endpointsCurrent())) return undefined;\n const deadline =\n Math.min(p.expiresAt, token.exp * 1000, grant?.expiresAt ?? Infinity) - FEDERATION_FRESHNESS.clockSkew;\n const admittedAt = ports.now();\n if (!Number.isSafeInteger(admittedAt) || admittedAt < now || admittedAt >= deadline) return undefined;\n const scope = JSON.stringify([\n ports.hostId,\n input.networkId,\n principal.issuer,\n principal.subject,\n caller.via?.callingHostId ?? p.deviceKeyId,\n ]);\n if (!(await ports.replay.admit(scope, p.nonce, p.expiresAt, admittedAt))) return undefined;\n const completedAt = ports.now();\n return Number.isSafeInteger(completedAt) &&\n completedAt >= admittedAt &&\n completedAt < deadline &&\n (await endpointsCurrent())\n ? caller\n : undefined;\n } catch {\n return undefined;\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { FederationValidity, SignedFederationEnvelope } from "../../types/federation.mjs";
|
|
2
|
+
//#region src/services/federationVerifier/type.d.ts
|
|
3
|
+
export interface FederationPrincipal {
|
|
4
|
+
issuer: string;
|
|
5
|
+
subject: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FederationCaller {
|
|
8
|
+
principal: FederationPrincipal;
|
|
9
|
+
org: string;
|
|
10
|
+
device: {
|
|
11
|
+
keyId: string;
|
|
12
|
+
};
|
|
13
|
+
via?: {
|
|
14
|
+
callingHostId: string;
|
|
15
|
+
grantId: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface FederationTokenClaims {
|
|
19
|
+
iss: string;
|
|
20
|
+
sub: string;
|
|
21
|
+
aud: string;
|
|
22
|
+
org: string;
|
|
23
|
+
org_role: 'owner' | 'admin' | 'member';
|
|
24
|
+
cnf: {
|
|
25
|
+
jkt: string;
|
|
26
|
+
};
|
|
27
|
+
iat: number;
|
|
28
|
+
exp: number;
|
|
29
|
+
nbf?: number;
|
|
30
|
+
jti: string;
|
|
31
|
+
revocation_epoch?: number;
|
|
32
|
+
act?: {
|
|
33
|
+
hostId: string;
|
|
34
|
+
enrollmentGeneration?: number;
|
|
35
|
+
};
|
|
36
|
+
grant?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface FederationRequestProof extends FederationValidity, FederationPrincipal {
|
|
39
|
+
tokenHash: string;
|
|
40
|
+
deviceKeyId: string;
|
|
41
|
+
destinationHostId: string;
|
|
42
|
+
networkId: string;
|
|
43
|
+
workspaceId: string;
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
method: string;
|
|
46
|
+
canonicalTarget: string;
|
|
47
|
+
bodyDigest: string;
|
|
48
|
+
nonce: string;
|
|
49
|
+
idempotencyKey?: string;
|
|
50
|
+
callingHostId?: string;
|
|
51
|
+
grantId?: string;
|
|
52
|
+
delegatedTokenHash?: string;
|
|
53
|
+
actingPrincipal?: FederationPrincipal;
|
|
54
|
+
}
|
|
55
|
+
export interface FederationConsentGrant extends FederationValidity, FederationPrincipal {
|
|
56
|
+
grantId: string;
|
|
57
|
+
revocationEpoch?: number;
|
|
58
|
+
deviceKeyId: string;
|
|
59
|
+
networkId: string;
|
|
60
|
+
workspaceId: string;
|
|
61
|
+
callingHostId: string;
|
|
62
|
+
targetHostId: string;
|
|
63
|
+
callingEnrollmentGeneration?: number;
|
|
64
|
+
targetEnrollmentGeneration?: number;
|
|
65
|
+
operations: string[];
|
|
66
|
+
constraints: Record<string, string | number | boolean | null>;
|
|
67
|
+
notBefore: number;
|
|
68
|
+
redelegable: false;
|
|
69
|
+
}
|
|
70
|
+
/** Atomic insert-if-absent. Never evict live nonces to bound storage. Times are milliseconds. */
|
|
71
|
+
export interface FederationReplayStore {
|
|
72
|
+
admit(scope: string, nonce: string, expiresAt: number, now: number): Promise<boolean>;
|
|
73
|
+
}
|
|
74
|
+
export interface FederationJwk {
|
|
75
|
+
kty: 'EC';
|
|
76
|
+
crv: 'P-256';
|
|
77
|
+
x: string;
|
|
78
|
+
y: string;
|
|
79
|
+
kid: string;
|
|
80
|
+
alg?: 'ES256';
|
|
81
|
+
use?: 'sig';
|
|
82
|
+
}
|
|
83
|
+
/** Persist this record only after a local claim or authenticated network join. */
|
|
84
|
+
export interface FederationIssuerPin {
|
|
85
|
+
issuer: string;
|
|
86
|
+
jwks: {
|
|
87
|
+
keys: FederationJwk[];
|
|
88
|
+
};
|
|
89
|
+
/** Missing timestamps from older pins are stale. Milliseconds since epoch. */
|
|
90
|
+
fetchedAt?: number;
|
|
91
|
+
}
|
|
92
|
+
export interface FederationAuthority {
|
|
93
|
+
/** Current feed state. Adapter must fail closed when it cannot establish authority. */
|
|
94
|
+
deviceRevoked: boolean;
|
|
95
|
+
revocationEpoch: number;
|
|
96
|
+
devicePublicKey: string;
|
|
97
|
+
trust?: 'tofu' | 'approved' | 'hub' | 'recovered' | 'org-admin';
|
|
98
|
+
organizationId?: string | null;
|
|
99
|
+
}
|
|
100
|
+
export interface FederationVerifierPorts {
|
|
101
|
+
hostId: string;
|
|
102
|
+
now(): number;
|
|
103
|
+
replay: FederationReplayStore;
|
|
104
|
+
issuer(issuer: string): Promise<FederationIssuerPin | undefined>;
|
|
105
|
+
/** Fixed same-origin path, redirects MUST be refused by the adapter. No token URL is used. */
|
|
106
|
+
fetchJwks(origin: string): Promise<FederationIssuerPin['jwks']>;
|
|
107
|
+
saveIssuer(pin: FederationIssuerPin): Promise<void>;
|
|
108
|
+
authority(principal: FederationPrincipal, org: string, deviceKeyId: string): Promise<FederationAuthority | undefined>;
|
|
109
|
+
hostKey(hostId: string, networkId: string): Promise<string | undefined>;
|
|
110
|
+
/** Missing adapter means legacy generation 1. An unavailable current enrollment refuses access. */
|
|
111
|
+
hostEnrollmentGeneration?(hostId: string, networkId: string): Promise<number | undefined>;
|
|
112
|
+
/** Home-host workspace ACL, provider consent and grant constraints are checked here. */
|
|
113
|
+
authorize(caller: FederationCaller, input: FederationVerifyInput, grant?: FederationConsentGrant): Promise<boolean>;
|
|
114
|
+
}
|
|
115
|
+
export interface FederationVerifyInput {
|
|
116
|
+
token: string;
|
|
117
|
+
envelope: SignedFederationEnvelope<FederationRequestProof, 'po/request/v1' | 'po/host-request/v1'>;
|
|
118
|
+
grant?: SignedFederationEnvelope<FederationConsentGrant, 'po/grant/v1'>;
|
|
119
|
+
networkId: string;
|
|
120
|
+
org: string;
|
|
121
|
+
workspaceId: string;
|
|
122
|
+
sessionId?: string;
|
|
123
|
+
method: string;
|
|
124
|
+
canonicalTarget: string;
|
|
125
|
+
body: string;
|
|
126
|
+
operation: string;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//# sourceMappingURL=type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.d.mts","names":[],"sources":["../../../src/services/federationVerifier/type.ts"],"mappings":";;iBAEiB;EACf;EACA;;iBAEe;EACf,WAAW;EACX;EACA;IAAU;;EACV;IAAQ;IAAuB;;;iBAEhB;EACf;EACA;EACA;EACA;EACA;EACA;IAAO;;EACP;EACA;EACA;EACA;EACA;EACA;IAAQ;IAAgB;;EACxB;;iBAEe,+BAA+B,oBAAoB;EAClE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBAAkB;;iBAEH,+BAA+B,oBAAoB;EAClE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;;;iBAGe;EACf,MAAM,eAAe,eAAe,mBAAmB,cAAc;;iBAEtD;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAGe;EACf;EACA;IAAQ,MAAM;;;EAEd;;iBAEe;;EAEf;EACA;EACA;EACA;EACA;;iBAEe;EACf;EACA;EACA,QAAQ;EACR,OAAO,iBAAiB,QAAQ;;EAEhC,UAAU,iBAAiB,QAAQ;EACnC,WAAW,KAAK,sBAAsB;EACtC,UAAU,WAAW,qBAAqB,aAAa,sBAAsB,QAAQ;EACrF,QAAQ,gBAAgB,oBAAoB;;EAE5C,0BAA0B,gBAAgB,oBAAoB;;EAE9D,UAAU,QAAQ,kBAAkB,OAAO,uBAAuB,QAAQ,yBAAyB;;iBAEpF;EACf;EACA,UAAU,yBAAyB;EACnC,QAAQ,yBAAyB;EACjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e={token:6e5,delegatedToken:36e5,envelope:6e4,clockSkew:3e4,manifest:864e5,grant:2592e6,grantRenewalWindow:6048e5,roster:6e5,rosterRelayCache:3e4,jwks:36e5,jwksRefetch:6e4,accountPurge:2592e6},t=[`po/request/v1`,`po/host-request/v1`,`po/grant/v1`,`po/roster/v1`,`po/capabilities/v1`,`po/vote/v1`,`po/lease/v1`,`po/device-approval/v1`,`po/hub-enrollment/v1`,`po/hub-claim/v1`,`po/hub-removal/v1`,`po/hub-removal-receipt/v1`];function n(e){if(e===null||typeof e==`string`||typeof e==`boolean`||typeof e==`number`&&Number.isFinite(e))return JSON.stringify(e);if(Array.isArray(e)){if(Object.keys(e).length!==e.length)throw Error(`Envelope arrays must be dense JSON arrays`);return`[${e.map(n).join(`,`)}]`}if(typeof e==`object`&&e&&Object.getPrototypeOf(e)===Object.prototype)return`{${Object.keys(e).sort().map(t=>`${JSON.stringify(t)}:${n(Reflect.get(e,t))}`).join(`,`)}}`;throw Error(`Envelope fields must be finite JSON values`)}function r(e){if(!e||Object.getPrototypeOf(e)!==Object.prototype)throw Error(`Invalid envelope payload`);return Object.keys(e).sort().flatMap(t=>[t,n(Reflect.get(e,t))])}exports.FEDERATION_FRESHNESS=e,exports.FEDERATION_TAGS=t,exports.federationEnvelopeFields=r;
|
|
2
|
+
//# sourceMappingURL=federation.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation.cjs","names":["FEDERATION_FRESHNESS","FEDERATION_TAGS","canonicalJson","value","key","federationEnvelopeFields","payload"],"sources":["../../src/types/federation.ts"],"sourcesContent":["/** G8 durations in milliseconds. JWT NumericDate claims use seconds instead. */\nexport const FEDERATION_FRESHNESS = {\n token: 10 * 60_000,\n delegatedToken: 60 * 60_000,\n envelope: 60_000,\n clockSkew: 30_000,\n manifest: 24 * 60 * 60_000,\n grant: 30 * 24 * 60 * 60_000,\n grantRenewalWindow: 7 * 24 * 60 * 60_000,\n /** Roster-derived authority (host keys, links, campaigns) fails closed this long after the last good sync. */\n roster: 10 * 60_000,\n /** A relay may reuse one roster read this long; an epoch change or tombstone invalidates it sooner. */\n rosterRelayCache: 30_000,\n /** A known issuer `kid` is trusted this long after its JWKS fetch; the issuer overlaps keys at least this long. */\n jwks: 60 * 60_000,\n /** Unknown-`kid` JWKS fetches per issuer are at most this frequent. */\n jwksRefetch: 60_000,\n /** Cloud workspace purge after account or org deletion completes within this window. Purge starts at once. */\n accountPurge: 30 * 24 * 60 * 60_000,\n} as const;\n\nexport const FEDERATION_TAGS = [\n 'po/request/v1',\n 'po/host-request/v1',\n 'po/grant/v1',\n 'po/roster/v1',\n 'po/capabilities/v1',\n 'po/vote/v1',\n 'po/lease/v1',\n 'po/device-approval/v1',\n 'po/hub-enrollment/v1',\n 'po/hub-claim/v1',\n 'po/hub-removal/v1',\n 'po/hub-removal-receipt/v1',\n] as const;\n\nexport type FederationTag = (typeof FEDERATION_TAGS)[number];\n\nexport interface FederationValidity {\n /** Unix milliseconds, including not-before where present. */\n issuedAt: number;\n expiresAt: number;\n notBefore?: number;\n}\n\n/** Signer keys are supplied from a trusted registry, never from this envelope. */\nexport interface SignedFederationEnvelope<T extends FederationValidity, Tag extends FederationTag = FederationTag> {\n tag: Tag;\n payload: T;\n /** Base64url IEEE P1363 P-256 signature. */\n signature: string;\n}\n\n/** JSON only, with recursively sorted object keys and array order preserved. */\nfunction canonicalJson(value: unknown): string {\n if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);\n if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);\n if (Array.isArray(value)) {\n if (Object.keys(value).length !== value.length) throw new Error('Envelope arrays must be dense JSON arrays');\n return `[${value.map(canonicalJson).join(',')}]`;\n }\n if (typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype) {\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonicalJson(Reflect.get(value, key))}`)\n .join(',')}}`;\n }\n throw new Error('Envelope fields must be finite JSON values');\n}\n\n/** Ordered key/value pairs, including every payload field, fed to attestationBytes. */\nexport function federationEnvelopeFields(payload: FederationValidity): string[] {\n if (!payload || Object.getPrototypeOf(payload) !== Object.prototype) throw new Error('Invalid envelope payload');\n return Object.keys(payload)\n .sort()\n .flatMap((key) => [key, canonicalJson(Reflect.get(payload, key))]);\n}\n"],"mappings":"AACA,MAAaA,EAAuB,CAClC,MAAO,IACP,eAAgB,KAChB,SAAU,IACV,UAAW,IACX,SAAU,MACV,MAAO,OACP,mBAAoB,OAEpB,OAAQ,IAER,iBAAkB,IAElB,KAAM,KAEN,YAAa,IAEb,aAAc,MAChB,EAEaC,EAAkB,CAC7B,gBACA,qBACA,cACA,eACA,qBACA,aACA,cACA,wBACA,uBACA,kBACA,oBACA,2BACF,EAoBA,SAASC,EAAc,EAAwB,CAE7C,GADIC,IAAU,MAAQ,OAAOA,GAAU,UAAY,OAAOA,GAAU,WAChE,OAAOA,GAAU,UAAY,OAAO,SAASA,CAAK,EAAG,OAAO,KAAK,UAAUA,CAAK,EACpF,GAAI,MAAM,QAAQA,CAAK,EAAG,CACxB,GAAI,OAAO,KAAKA,CAAK,CAAC,CAAC,SAAWA,EAAM,OAAQ,MAAU,MAAM,2CAA2C,EAC3G,MAAO,IAAIA,EAAM,IAAID,CAAa,CAAC,CAAC,KAAK,GAAG,EAAE,EAChD,CACA,GAAI,OAAOC,GAAU,UAAYA,GAAkB,OAAO,eAAeA,CAAK,IAAM,OAAO,UACzF,MAAO,IAAI,OAAO,KAAKA,CAAK,CAAC,CAC1B,KAAK,CAAC,CACN,IAAK,GAAQ,GAAG,KAAK,UAAUC,CAAG,EAAE,GAAGF,EAAc,QAAQ,IAAIC,EAAOC,CAAG,CAAC,GAAG,CAAC,CAChF,KAAK,GAAG,EAAE,GAEf,MAAU,MAAM,4CAA4C,CAC9D,CAGA,SAAgBC,EAAyB,EAAuC,CAC9E,GAAI,CAACC,GAAW,OAAO,eAAeA,CAAO,IAAM,OAAO,UAAW,MAAU,MAAM,0BAA0B,EAC/G,OAAO,OAAO,KAAKA,CAAO,CAAC,CACxB,KAAK,CAAC,CACN,QAAS,GAAQ,CAACF,EAAKF,EAAc,QAAQ,IAAII,EAASF,CAAG,CAAC,CAAC,CAAC,CACrE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/types/federation.d.ts
|
|
2
|
+
/** G8 durations in milliseconds. JWT NumericDate claims use seconds instead. */
|
|
3
|
+
export declare const FEDERATION_FRESHNESS: {
|
|
4
|
+
readonly token: number;
|
|
5
|
+
readonly delegatedToken: number;
|
|
6
|
+
readonly envelope: 60000;
|
|
7
|
+
readonly clockSkew: 30000;
|
|
8
|
+
readonly manifest: number;
|
|
9
|
+
readonly grant: number;
|
|
10
|
+
readonly grantRenewalWindow: number;
|
|
11
|
+
/** Roster-derived authority (host keys, links, campaigns) fails closed this long after the last good sync. */
|
|
12
|
+
readonly roster: number;
|
|
13
|
+
/** A relay may reuse one roster read this long; an epoch change or tombstone invalidates it sooner. */
|
|
14
|
+
readonly rosterRelayCache: 30000;
|
|
15
|
+
/** A known issuer `kid` is trusted this long after its JWKS fetch; the issuer overlaps keys at least this long. */
|
|
16
|
+
readonly jwks: number;
|
|
17
|
+
/** Unknown-`kid` JWKS fetches per issuer are at most this frequent. */
|
|
18
|
+
readonly jwksRefetch: 60000;
|
|
19
|
+
/** Cloud workspace purge after account or org deletion completes within this window. Purge starts at once. */
|
|
20
|
+
readonly accountPurge: number;
|
|
21
|
+
};
|
|
22
|
+
export declare const FEDERATION_TAGS: readonly ['po/request/v1', 'po/host-request/v1', 'po/grant/v1', 'po/roster/v1', 'po/capabilities/v1', 'po/vote/v1', 'po/lease/v1', 'po/device-approval/v1', 'po/hub-enrollment/v1', 'po/hub-claim/v1', 'po/hub-removal/v1', 'po/hub-removal-receipt/v1'];
|
|
23
|
+
export type FederationTag = (typeof FEDERATION_TAGS)[number];
|
|
24
|
+
export interface FederationValidity {
|
|
25
|
+
/** Unix milliseconds, including not-before where present. */
|
|
26
|
+
issuedAt: number;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
notBefore?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Signer keys are supplied from a trusted registry, never from this envelope. */
|
|
31
|
+
export interface SignedFederationEnvelope<T extends FederationValidity, Tag extends FederationTag = FederationTag> {
|
|
32
|
+
tag: Tag;
|
|
33
|
+
payload: T;
|
|
34
|
+
/** Base64url IEEE P1363 P-256 signature. */
|
|
35
|
+
signature: string;
|
|
36
|
+
}
|
|
37
|
+
/** Ordered key/value pairs, including every payload field, fed to attestationBytes. */
|
|
38
|
+
export declare function federationEnvelopeFields(payload: FederationValidity): string[];
|
|
39
|
+
//#endregion
|
|
40
|
+
//# sourceMappingURL=federation.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation.d.mts","names":[],"sources":["../../src/types/federation.ts"],"mappings":";;qBACa;WACX;WACA;WACA;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;qBAGW;YAeD,wBAAwB;iBAEnB;;EAEf;EACA;EACA;;;iBAIe,yBAAyB,UAAU,oBAAoB,YAAY,gBAAgB;EAClG,KAAK;EACL,SAAS;;EAET;;;wBAqBc,yBAAyB,SAAS"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e={token:6e5,delegatedToken:36e5,envelope:6e4,clockSkew:3e4,manifest:864e5,grant:2592e6,grantRenewalWindow:6048e5,roster:6e5,rosterRelayCache:3e4,jwks:36e5,jwksRefetch:6e4,accountPurge:2592e6},t=[`po/request/v1`,`po/host-request/v1`,`po/grant/v1`,`po/roster/v1`,`po/capabilities/v1`,`po/vote/v1`,`po/lease/v1`,`po/device-approval/v1`,`po/hub-enrollment/v1`,`po/hub-claim/v1`,`po/hub-removal/v1`,`po/hub-removal-receipt/v1`];function n(e){if(e===null||typeof e==`string`||typeof e==`boolean`||typeof e==`number`&&Number.isFinite(e))return JSON.stringify(e);if(Array.isArray(e)){if(Object.keys(e).length!==e.length)throw Error(`Envelope arrays must be dense JSON arrays`);return`[${e.map(n).join(`,`)}]`}if(typeof e==`object`&&e&&Object.getPrototypeOf(e)===Object.prototype)return`{${Object.keys(e).sort().map(t=>`${JSON.stringify(t)}:${n(Reflect.get(e,t))}`).join(`,`)}}`;throw Error(`Envelope fields must be finite JSON values`)}function r(e){if(!e||Object.getPrototypeOf(e)!==Object.prototype)throw Error(`Invalid envelope payload`);return Object.keys(e).sort().flatMap(t=>[t,n(Reflect.get(e,t))])}export{e as FEDERATION_FRESHNESS,t as FEDERATION_TAGS,r as federationEnvelopeFields};
|
|
2
|
+
//# sourceMappingURL=federation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation.mjs","names":["FEDERATION_FRESHNESS","FEDERATION_TAGS","canonicalJson","value","key","federationEnvelopeFields","payload"],"sources":["../../src/types/federation.ts"],"sourcesContent":["/** G8 durations in milliseconds. JWT NumericDate claims use seconds instead. */\nexport const FEDERATION_FRESHNESS = {\n token: 10 * 60_000,\n delegatedToken: 60 * 60_000,\n envelope: 60_000,\n clockSkew: 30_000,\n manifest: 24 * 60 * 60_000,\n grant: 30 * 24 * 60 * 60_000,\n grantRenewalWindow: 7 * 24 * 60 * 60_000,\n /** Roster-derived authority (host keys, links, campaigns) fails closed this long after the last good sync. */\n roster: 10 * 60_000,\n /** A relay may reuse one roster read this long; an epoch change or tombstone invalidates it sooner. */\n rosterRelayCache: 30_000,\n /** A known issuer `kid` is trusted this long after its JWKS fetch; the issuer overlaps keys at least this long. */\n jwks: 60 * 60_000,\n /** Unknown-`kid` JWKS fetches per issuer are at most this frequent. */\n jwksRefetch: 60_000,\n /** Cloud workspace purge after account or org deletion completes within this window. Purge starts at once. */\n accountPurge: 30 * 24 * 60 * 60_000,\n} as const;\n\nexport const FEDERATION_TAGS = [\n 'po/request/v1',\n 'po/host-request/v1',\n 'po/grant/v1',\n 'po/roster/v1',\n 'po/capabilities/v1',\n 'po/vote/v1',\n 'po/lease/v1',\n 'po/device-approval/v1',\n 'po/hub-enrollment/v1',\n 'po/hub-claim/v1',\n 'po/hub-removal/v1',\n 'po/hub-removal-receipt/v1',\n] as const;\n\nexport type FederationTag = (typeof FEDERATION_TAGS)[number];\n\nexport interface FederationValidity {\n /** Unix milliseconds, including not-before where present. */\n issuedAt: number;\n expiresAt: number;\n notBefore?: number;\n}\n\n/** Signer keys are supplied from a trusted registry, never from this envelope. */\nexport interface SignedFederationEnvelope<T extends FederationValidity, Tag extends FederationTag = FederationTag> {\n tag: Tag;\n payload: T;\n /** Base64url IEEE P1363 P-256 signature. */\n signature: string;\n}\n\n/** JSON only, with recursively sorted object keys and array order preserved. */\nfunction canonicalJson(value: unknown): string {\n if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);\n if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);\n if (Array.isArray(value)) {\n if (Object.keys(value).length !== value.length) throw new Error('Envelope arrays must be dense JSON arrays');\n return `[${value.map(canonicalJson).join(',')}]`;\n }\n if (typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype) {\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonicalJson(Reflect.get(value, key))}`)\n .join(',')}}`;\n }\n throw new Error('Envelope fields must be finite JSON values');\n}\n\n/** Ordered key/value pairs, including every payload field, fed to attestationBytes. */\nexport function federationEnvelopeFields(payload: FederationValidity): string[] {\n if (!payload || Object.getPrototypeOf(payload) !== Object.prototype) throw new Error('Invalid envelope payload');\n return Object.keys(payload)\n .sort()\n .flatMap((key) => [key, canonicalJson(Reflect.get(payload, key))]);\n}\n"],"mappings":"AACA,MAAaA,EAAuB,CAClC,MAAO,IACP,eAAgB,KAChB,SAAU,IACV,UAAW,IACX,SAAU,MACV,MAAO,OACP,mBAAoB,OAEpB,OAAQ,IAER,iBAAkB,IAElB,KAAM,KAEN,YAAa,IAEb,aAAc,MAChB,EAEaC,EAAkB,CAC7B,gBACA,qBACA,cACA,eACA,qBACA,aACA,cACA,wBACA,uBACA,kBACA,oBACA,2BACF,EAoBA,SAASC,EAAc,EAAwB,CAE7C,GADIC,IAAU,MAAQ,OAAOA,GAAU,UAAY,OAAOA,GAAU,WAChE,OAAOA,GAAU,UAAY,OAAO,SAASA,CAAK,EAAG,OAAO,KAAK,UAAUA,CAAK,EACpF,GAAI,MAAM,QAAQA,CAAK,EAAG,CACxB,GAAI,OAAO,KAAKA,CAAK,CAAC,CAAC,SAAWA,EAAM,OAAQ,MAAU,MAAM,2CAA2C,EAC3G,MAAO,IAAIA,EAAM,IAAID,CAAa,CAAC,CAAC,KAAK,GAAG,EAAE,EAChD,CACA,GAAI,OAAOC,GAAU,UAAYA,GAAkB,OAAO,eAAeA,CAAK,IAAM,OAAO,UACzF,MAAO,IAAI,OAAO,KAAKA,CAAK,CAAC,CAC1B,KAAK,CAAC,CACN,IAAK,GAAQ,GAAG,KAAK,UAAUC,CAAG,EAAE,GAAGF,EAAc,QAAQ,IAAIC,EAAOC,CAAG,CAAC,GAAG,CAAC,CAChF,KAAK,GAAG,EAAE,GAEf,MAAU,MAAM,4CAA4C,CAC9D,CAGA,SAAgBC,EAAyB,EAAuC,CAC9E,GAAI,CAACC,GAAW,OAAO,eAAeA,CAAO,IAAM,OAAO,UAAW,MAAU,MAAM,0BAA0B,EAC/G,OAAO,OAAO,KAAKA,CAAO,CAAC,CACxB,KAAK,CAAC,CACN,QAAS,GAAQ,CAACF,EAAKF,EAAc,QAAQ,IAAII,EAASF,CAAG,CAAC,CAAC,CAAC,CACrE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/po-web-security",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.54",
|
|
4
4
|
"description": "Shared security primitives for the Po web cockpit: sealed channels and signed bundle manifests for independently trusted verifiers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
"import": "./dist/node.mjs",
|
|
57
57
|
"require": "./dist/node.cjs"
|
|
58
58
|
},
|
|
59
|
-
"./package.json": "./package.json"
|
|
59
|
+
"./package.json": "./package.json",
|
|
60
|
+
"./deviceProof": {
|
|
61
|
+
"types": "./dist/deviceProof.d.mts",
|
|
62
|
+
"import": "./dist/deviceProof.mjs",
|
|
63
|
+
"require": "./dist/deviceProof.cjs"
|
|
64
|
+
}
|
|
60
65
|
},
|
|
61
66
|
"publishConfig": {
|
|
62
67
|
"access": "public"
|