/**
* Does this release actually come from the identity that owns the name?
*
* The release registry proves a release was signed by the device key it names
* (lib/release-registry.js). It says nothing about whether that device is
* allowed to publish under this app NAME — the registry and the name registry
* are separate structures, and a signature only binds an entry to whatever key
* happens to be in it.
*
* The missing link is the NameRegistry claim, which records the publisher's
* identityRoot. This module joins the two: resolve the entry's device key back
* to its root via the attestation chain, then require that root to be the one
* the claim is anchored to.
*
* That is why the anchor had to be the ROOT and not a device key (org #142) —
* bound to a device, this check would refuse every release published from any
* other device the identity owns, and would have to be re-pointed on every
* device rotation.
*
* TRANSITIONAL, deliberately (org #144 step 3):
*
* - Entries published before `publisherChain` existed carry no chain.
* - Claims made before #142 are anchored to a device key, not a root.
* - An app installed by pgh:// URL may have no name claim at all.
*
* So a missing piece is `indeterminate`, not a refusal. Only a present-and-
* contradictory pair is refused. Enforcement — treating indeterminate as a
* refusal — is a later flip, once publishers have re-published and the
* existing claims have been re-anchored with `pgh names update`.
*/
import IdentityKey from 'keet-identity-key'
import b4a from 'b4a'
export const TRUST_OK = 'ok'
export const TRUST_INDETERMINATE = 'indeterminate'
export const TRUST_REFUSED = 'refused'
/**
* @param {object} entry release entry ({ publisherDevicePubkey, publisherChain })
* @param {object} claim NameRegistry claim ({ publisher: { identityRoot } })
* @returns {{ status: string, reason: string, root?: string }}
*/
export function checkPublisherTrust (entry, claim) {
if (!entry) return { status: TRUST_INDETERMINATE, reason: 'no release entry' }
const claimedRoot = claim && claim.publisher && claim.publisher.identityRoot
if (!claimedRoot) {
return { status: TRUST_INDETERMINATE, reason: 'name is not claimed, so there is no root to check against' }
}
const device = entry.publisherDevicePubkey
if (!device) {
return { status: TRUST_INDETERMINATE, reason: 'entry names no publisher device key' }
}
// A claim still anchored to a device key (pre-#142) cannot answer the
// question — matching it would only prove "same device", which is the
// property we are deliberately not depending on.
if (claimedRoot === device && !entry.publisherChain) {
return {
status: TRUST_INDETERMINATE,
reason: 'claim is anchored to a device key, not a root (pre-#142 claim; re-anchor with `pgh names update`)'
}
}
if (!entry.publisherChain) {
return {
status: TRUST_INDETERMINATE,
reason: 'entry carries no attestation chain (published before signing was wired)'
}
}
let verified
try {
const proof = b4a.from(entry.publisherChain, 'base64')
if (!proof || proof.length === 0) {
return { status: TRUST_REFUSED, reason: 'attestation chain is empty' }
}
verified = IdentityKey.verify(proof)
} catch (err) {
return { status: TRUST_REFUSED, reason: 'attestation chain did not parse: ' + err.message }
}
if (!verified || !verified.identityPublicKey || !verified.devicePublicKey) {
return { status: TRUST_REFUSED, reason: 'attestation chain failed verification' }
}
// The chain must describe THIS entry's device, or it proves nothing about
// this release — it could be any valid chain lifted from anywhere.
const chainDevice = b4a.toString(verified.devicePublicKey, 'hex')
if (chainDevice !== device) {
return {
status: TRUST_REFUSED,
reason: 'chain attests device ' + chainDevice.slice(0, 12) + '… but the entry was signed by ' +
String(device).slice(0, 12) + '…'
}
}
const root = b4a.toString(verified.identityPublicKey, 'hex')
if (root !== claimedRoot) {
return {
status: TRUST_REFUSED,
reason: 'release publisher resolves to identity ' + root.slice(0, 12) +
'… but the name is owned by ' + String(claimedRoot).slice(0, 12) + '…',
root
}
}
return { status: TRUST_OK, reason: 'publisher device chains to the identity that owns the name', root }
}
/**
* Throwing form for install paths. Refuses only a contradiction; an
* indeterminate result passes and is returned so the caller can report it.
*/
export function assertPublisherTrust (entry, claim, { name } = {}) {
const result = checkPublisherTrust(entry, claim)
if (result.status === TRUST_REFUSED) {
throw Object.assign(
new Error(
'refusing ' + (name || (entry && entry.name)) + '@' + (entry && entry.version) +
': ' + result.reason
),
{ code: 'EPUBLISHERMISMATCH', result }
)
}
return result
}