From 8a1f8ad5867272a94c5b93020db5a6a0fd1ade91 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Fri, 30 Sep 2022 23:37:55 +0200 Subject: [PATCH 1/9] Allows multiple claim methods --- src/verifications.js | 53 +++++++++++++++++++++++------------ test/claimDefinitions.test.js | 6 ++-- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/verifications.js b/src/verifications.js index c5ce662..41f9637 100644 --- a/src/verifications.js +++ b/src/verifications.js @@ -200,33 +200,50 @@ const run = async (proofData, claimData, fingerprint) => { errors: [] } + const claimMethods = Array.isArray(claimData.claim) + ? claimData.claim + : [claimData.claim] + switch (claimData.proof.request.format) { case E.ProofFormat.JSON: - try { - res.result = await runJSON( - proofData, - claimData.claim.path, - fingerprint, - claimData.claim.format, - claimData.claim.relation - ) - res.completed = true - } catch (error) { - res.errors.push(error.message ? error.message : error) + for (let index = 0; index < claimMethods.length; index++) { + const claimMethod = claimMethods[index] + try { + res.result = res.result || await runJSON( + proofData, + claimMethod.path, + fingerprint, + claimMethod.format, + claimMethod.relation + ) + } catch (error) { + res.errors.push(error.message ? error.message : error) + } } + res.completed = true break case E.ProofFormat.TEXT: - try { - res.result = await containsProof(proofData, - fingerprint, - claimData.claim.format) - res.completed = true - } catch (error) { - res.errors.push('err_unknown_text_verification') + for (let index = 0; index < claimMethods.length; index++) { + const claimMethod = claimMethods[index] + try { + res.result = res.result || await containsProof( + proofData, + fingerprint, + claimMethod.format + ) + } catch (error) { + res.errors.push('err_unknown_text_verification') + } } + res.completed = true break } + // Reset the errors if one of the claim methods was successful + if (res.result) { + res.errors = [] + } + return res } diff --git a/test/claimDefinitions.test.js b/test/claimDefinitions.test.js index 3f494ae..e580ff0 100644 --- a/test/claimDefinitions.test.js +++ b/test/claimDefinitions.test.js @@ -49,10 +49,8 @@ const pattern = { data: _.isObject, }, }, - claim: { - format: _.isInteger, - relation: _.isInteger, - path: _.isArray, + claim: (x) => { + return _.isObject(x) || _.isArray(x) }, } From e28315e87f8e44783eb661e1cb5cdd3dd8244f47 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Fri, 30 Sep 2022 23:39:10 +0200 Subject: [PATCH 2/9] Add activitypub service provider --- src/claimDefinitions/activitypub.js | 85 +++++++++++++++++++ src/claimDefinitions/index.js | 25 +----- src/enums.js | 12 +-- src/fetcher/activitypub.js | 121 ++++++++++++++++++++++++++++ src/fetcher/index.js | 1 + 5 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 src/claimDefinitions/activitypub.js create mode 100644 src/fetcher/activitypub.js diff --git a/src/claimDefinitions/activitypub.js b/src/claimDefinitions/activitypub.js new file mode 100644 index 0000000..8fc7d0d --- /dev/null +++ b/src/claimDefinitions/activitypub.js @@ -0,0 +1,85 @@ +/* +Copyright 2021 Yarmo Mackenbach + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const E = require('../enums') + +const reURI = /^acct:(.*)@(.*)\/?/ + +const processURI = (uri) => { + const match = uri.match(reURI) + + return { + serviceprovider: { + type: 'web', + name: 'activitypub' + }, + match: { + regularExpression: reURI, + isAmbiguous: false + }, + profile: { + display: `${match[1]}@${match[2]}`, + uri: uri, + qr: null + }, + proof: { + uri: uri, + request: { + fetcher: E.Fetcher.ACTIVITYPUB, + access: E.ProofAccess.GENERIC, + format: E.ProofFormat.JSON, + data: { + username: match[1], + domain: match[2] + } + } + }, + claim: [ + { + format: E.ClaimFormat.FINGERPRINT, + relation: E.ClaimRelation.CONTAINS, + path: ['summary'] + }, + { + format: E.ClaimFormat.FINGERPRINT, + relation: E.ClaimRelation.CONTAINS, + path: ['attachment', 'value'] + } + ] + } +} + +const tests = [ + { + uri: 'acct:alice@domain.org', + shouldMatch: true + }, + { + uri: 'acct:alice', + shouldMatch: false + }, + { + uri: 'https://domain.org/@alice/', + shouldMatch: false + }, + { + uri: 'https://domain.org/alice', + shouldMatch: false + } +] + +exports.reURI = reURI +exports.processURI = processURI +exports.tests = tests diff --git a/src/claimDefinitions/index.js b/src/claimDefinitions/index.js index dcc7b81..5046ca8 100644 --- a/src/claimDefinitions/index.js +++ b/src/claimDefinitions/index.js @@ -13,28 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -const list = [ - 'dns', - 'irc', - 'xmpp', - 'matrix', - 'telegram', - 'twitter', - 'reddit', - 'liberapay', - 'lichess', - 'hackernews', - 'lobsters', - 'devto', - 'gitea', - 'gitlab', - 'github', - 'mastodon', - 'pleroma', - 'discourse', - 'owncast', - 'stackexchange' -] const data = { dns: require('./dns'), @@ -52,6 +30,7 @@ const data = { gitea: require('./gitea'), gitlab: require('./gitlab'), github: require('./github'), + activitypub: require('./activitypub'), mastodon: require('./mastodon'), pleroma: require('./pleroma'), discourse: require('./discourse'), @@ -59,5 +38,5 @@ const data = { stackexchange: require('./stackexchange') } -exports.list = list +exports.list = Object.keys(data) exports.data = data diff --git a/src/enums.js b/src/enums.js index c2813fe..e15486a 100644 --- a/src/enums.js +++ b/src/enums.js @@ -39,20 +39,22 @@ Object.freeze(ProxyPolicy) * @enum {string} */ const Fetcher = { - /** Basic HTTP requests */ - HTTP: 'http', + /** HTTP requests to ActivityPub */ + ACTIVITYPUB: 'activitypub', /** DNS module from Node.js */ DNS: 'dns', + /** Basic HTTP requests */ + HTTP: 'http', /** IRC module from Node.js */ IRC: 'irc', - /** XMPP module from Node.js */ - XMPP: 'xmpp', /** HTTP request to Matrix API */ MATRIX: 'matrix', /** HTTP request to Telegram API */ TELEGRAM: 'telegram', /** HTTP request to Twitter API */ - TWITTER: 'twitter' + TWITTER: 'twitter', + /** XMPP module from Node.js */ + XMPP: 'xmpp' } Object.freeze(Fetcher) diff --git a/src/fetcher/activitypub.js b/src/fetcher/activitypub.js new file mode 100644 index 0000000..8325e3a --- /dev/null +++ b/src/fetcher/activitypub.js @@ -0,0 +1,121 @@ +/* +Copyright 2021 Yarmo Mackenbach + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const axios = require('axios') +const jsEnv = require('browser-or-node') + +/** + * @module fetcher/activitypub + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ +module.exports.timeout = 5000 + +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.username - The username of the account to verify + * @param {string} data.domain - The domain of the ActivityPub instance + * @param {object} opts - Options used to enable the request + * @param {string} opts.claims.activitypub.acct - The identifier of the verifier account + * @param {string} opts.claims.activitypub.privateKey - The private key to sign the request + * @returns {object} + */ +module.exports.fn = async (data, opts) => { + let crypto + if (jsEnv.isNode) { + crypto = require('crypto') + } + + let timeoutHandle + const timeoutPromise = new Promise((resolve, reject) => { + timeoutHandle = setTimeout( + () => reject(new Error('Request was timed out')), + data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout + ) + }) + + const fetchPromise = new Promise((resolve, reject) => { + (async () => { + if (!opts.claims.activitypub.acct.match(/acct:(.*)@(.*)/)) { + reject(new Error('ActivityPub fetcher was not set up properly')) + } + + const urlWebfinger = `https://${data.domain}/.well-known/webfinger?resource=acct:${data.username}@${data.domain}` + const webfinger = await axios.get(urlWebfinger, + { + headers: { Accept: 'application/json' } + }) + .then(res => { + return res.data + }) + .catch(error => { + reject(error) + }) + + let urlActivitypub = null + webfinger.links.forEach(element => { + if (element.type === 'application/activity+json') { + urlActivitypub = element.href + } + }) + + // Prepare the signature + const matchAcct = opts.claims.activitypub.acct.match(/acct:(.*)@(.*)/) + const now = new Date() + const { host, pathname, search } = new URL(urlActivitypub) + const signedString = `(request-target): get ${pathname}${search}\nhost: ${host}\ndate: ${now.toUTCString()}` + + const headers = { + host, + date: now.toUTCString(), + accept: 'application/activity+json' + } + + if (jsEnv.isNode) { + // Generate the signature + const sign = crypto.createSign('SHA256') + sign.write(signedString) + sign.end() + const signatureSig = sign.sign(opts.claims.activitypub.privateKey, 'base64') + headers.signature = `keyId="https://${matchAcct[2]}/${matchAcct[1]}#main-key",headers="(request-target) host date",signature="${signatureSig}",algorithm="rsa-sha256"` + } + + axios.get(urlActivitypub, + { + headers + }) + .then(res => { + return res.data + }) + .then(res => { + resolve(res) + }) + .catch(error => { + reject(error) + }) + })() + }) + + return Promise.race([fetchPromise, timeoutPromise]).then((result) => { + clearTimeout(timeoutHandle) + return result + }) +} diff --git a/src/fetcher/index.js b/src/fetcher/index.js index df32047..ffc19e8 100644 --- a/src/fetcher/index.js +++ b/src/fetcher/index.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +exports.activitypub = require('./activitypub') exports.dns = require('./dns') exports.http = require('./http') exports.irc = require('./irc') From 007282ef22b219b20ec3d4bf04e4b9f49a2e3da7 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Sat, 1 Oct 2022 09:53:36 +0200 Subject: [PATCH 3/9] Add AP proxy endpoint --- src/proxy/api/v2/index.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/proxy/api/v2/index.js b/src/proxy/api/v2/index.js index 18f7a17..674150f 100644 --- a/src/proxy/api/v2/index.js +++ b/src/proxy/api/v2/index.js @@ -246,4 +246,26 @@ router.get( } ) +// ActivityPub route +router.get( + '/get/activitypub', + query('username').isString(), + query('domain').isFQDN(), + async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }) + } + + fetcher.activitypub + .fn(req.query, opts) + .then((data) => { + return res.status(200).send(data) + }) + .catch((err) => { + return res.status(400).json({ errors: err.message ? err.message : err }) + }) + } +) + module.exports = router From 17101952110a09c077c8428b521bd08d81b4624f Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Sat, 1 Oct 2022 09:57:58 +0200 Subject: [PATCH 4/9] Update default options --- src/defaults.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/defaults.js b/src/defaults.js index 8c80cb1..2fd03ae 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -59,6 +59,10 @@ const opts = { }, twitter: { bearerToken: null + }, + activitypub: { + acct: null, + privateKey: null } } } From 2a298321104999475f640d3d6a3d14a3593580f4 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Mon, 3 Oct 2022 22:32:46 +0200 Subject: [PATCH 5/9] Remove webfinger from activitypub --- src/claim.js | 7 +++++- src/claimDefinitions/activitypub.js | 37 ++++++++++++++++++---------- src/fetcher/activitypub.js | 38 ++++++++--------------------- src/proxy/api/v2/index.js | 3 +-- test/claimDefinitions.test.js | 3 +++ 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/claim.js b/src/claim.js index f8298e2..3580123 100644 --- a/src/claim.js +++ b/src/claim.js @@ -220,7 +220,7 @@ class Claim { // For each match for (let index = 0; index < this._matches.length; index++) { - const claimData = this._matches[index] + let claimData = this._matches[index] let verificationResult = null let proofData = null @@ -243,6 +243,11 @@ class Claim { fetcher: proofData.fetcher, viaProxy: proofData.viaProxy } + + // Post process the data + if (claimData.functions && claimData.functions.postprocess) { + ({ claimData, proofData } = claimData.functions.postprocess(claimData, proofData)) + } } else { // Consider the proof completed but with a negative result verificationResult = verificationResult || { diff --git a/src/claimDefinitions/activitypub.js b/src/claimDefinitions/activitypub.js index 8fc7d0d..c0e34f7 100644 --- a/src/claimDefinitions/activitypub.js +++ b/src/claimDefinitions/activitypub.js @@ -15,11 +15,9 @@ limitations under the License. */ const E = require('../enums') -const reURI = /^acct:(.*)@(.*)\/?/ +const reURI = /^https:\/\/(.*)\/?/ const processURI = (uri) => { - const match = uri.match(reURI) - return { serviceprovider: { type: 'web', @@ -30,7 +28,7 @@ const processURI = (uri) => { isAmbiguous: false }, profile: { - display: `${match[1]}@${match[2]}`, + display: uri, uri: uri, qr: null }, @@ -41,8 +39,7 @@ const processURI = (uri) => { access: E.ProofAccess.GENERIC, format: E.ProofFormat.JSON, data: { - username: match[1], - domain: match[2] + url: uri } } }, @@ -57,25 +54,39 @@ const processURI = (uri) => { relation: E.ClaimRelation.CONTAINS, path: ['attachment', 'value'] } - ] + ], + functions: { + postprocess: (claimData, proofData) => { + claimData.profile.display = `${proofData.result.preferredUsername}@${new URL(proofData.result.url).hostname}` + return { claimData, proofData } + } + } } } const tests = [ { - uri: 'acct:alice@domain.org', + uri: 'https://domain.org', shouldMatch: true }, { - uri: 'acct:alice', - shouldMatch: false + uri: 'https://domain.org/@/alice/', + shouldMatch: true }, { - uri: 'https://domain.org/@alice/', - shouldMatch: false + uri: 'https://domain.org/@alice', + shouldMatch: true }, { - uri: 'https://domain.org/alice', + uri: 'https://domain.org/u/alice/', + shouldMatch: true + }, + { + uri: 'https://domain.org/users/alice/', + shouldMatch: true + }, + { + uri: 'http://domain.org/alice', shouldMatch: false } ] diff --git a/src/fetcher/activitypub.js b/src/fetcher/activitypub.js index 8325e3a..a9c1719 100644 --- a/src/fetcher/activitypub.js +++ b/src/fetcher/activitypub.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ const axios = require('axios') +const validator = require('validator') const jsEnv = require('browser-or-node') /** @@ -31,10 +32,9 @@ module.exports.timeout = 5000 * @function * @async * @param {object} data - Data used in the request - * @param {string} data.username - The username of the account to verify - * @param {string} data.domain - The domain of the ActivityPub instance + * @param {string} data.url - The URL of the account to verify * @param {object} opts - Options used to enable the request - * @param {string} opts.claims.activitypub.acct - The identifier of the verifier account + * @param {string} opts.claims.activitypub.url - The URL of the verifier account * @param {string} opts.claims.activitypub.privateKey - The private key to sign the request * @returns {object} */ @@ -54,33 +54,15 @@ module.exports.fn = async (data, opts) => { const fetchPromise = new Promise((resolve, reject) => { (async () => { - if (!opts.claims.activitypub.acct.match(/acct:(.*)@(.*)/)) { - reject(new Error('ActivityPub fetcher was not set up properly')) + try { + validator.isURL(opts.claims.activitypub.url) + } catch (err) { + throw new Error(`ActivityPub fetcher was not set up properly (${err.message})`) } - const urlWebfinger = `https://${data.domain}/.well-known/webfinger?resource=acct:${data.username}@${data.domain}` - const webfinger = await axios.get(urlWebfinger, - { - headers: { Accept: 'application/json' } - }) - .then(res => { - return res.data - }) - .catch(error => { - reject(error) - }) - - let urlActivitypub = null - webfinger.links.forEach(element => { - if (element.type === 'application/activity+json') { - urlActivitypub = element.href - } - }) - // Prepare the signature - const matchAcct = opts.claims.activitypub.acct.match(/acct:(.*)@(.*)/) const now = new Date() - const { host, pathname, search } = new URL(urlActivitypub) + const { host, pathname, search } = new URL(data.url) const signedString = `(request-target): get ${pathname}${search}\nhost: ${host}\ndate: ${now.toUTCString()}` const headers = { @@ -95,10 +77,10 @@ module.exports.fn = async (data, opts) => { sign.write(signedString) sign.end() const signatureSig = sign.sign(opts.claims.activitypub.privateKey, 'base64') - headers.signature = `keyId="https://${matchAcct[2]}/${matchAcct[1]}#main-key",headers="(request-target) host date",signature="${signatureSig}",algorithm="rsa-sha256"` + headers.signature = `keyId="${opts.claims.activitypub.url}#main-key",headers="(request-target) host date",signature="${signatureSig}",algorithm="rsa-sha256"` } - axios.get(urlActivitypub, + axios.get(data.url, { headers }) diff --git a/src/proxy/api/v2/index.js b/src/proxy/api/v2/index.js index 674150f..715d27c 100644 --- a/src/proxy/api/v2/index.js +++ b/src/proxy/api/v2/index.js @@ -249,8 +249,7 @@ router.get( // ActivityPub route router.get( '/get/activitypub', - query('username').isString(), - query('domain').isFQDN(), + query('url').isURL(), async (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { diff --git a/test/claimDefinitions.test.js b/test/claimDefinitions.test.js index e580ff0..66b78b8 100644 --- a/test/claimDefinitions.test.js +++ b/test/claimDefinitions.test.js @@ -52,6 +52,9 @@ const pattern = { claim: (x) => { return _.isObject(x) || _.isArray(x) }, + functions: (x) => { + return _.isObject(x) || _.isUndefined(x) + }, } doipjs.claimDefinitions.list.forEach((claimDefName, i) => { From 3c1302c150d641cce886a8891b09b5fbe4fca269 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Fri, 14 Oct 2022 13:02:49 +0200 Subject: [PATCH 6/9] Fix minor bug, allow not setting up --- src/fetcher/activitypub.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fetcher/activitypub.js b/src/fetcher/activitypub.js index a9c1719..89eff8d 100644 --- a/src/fetcher/activitypub.js +++ b/src/fetcher/activitypub.js @@ -54,16 +54,16 @@ module.exports.fn = async (data, opts) => { const fetchPromise = new Promise((resolve, reject) => { (async () => { + let isConfigured = false try { validator.isURL(opts.claims.activitypub.url) + isConfigured = true } catch (err) { - throw new Error(`ActivityPub fetcher was not set up properly (${err.message})`) + console.log(`ActivityPub fetcher was not set up (${err.message})`) } - // Prepare the signature const now = new Date() const { host, pathname, search } = new URL(data.url) - const signedString = `(request-target): get ${pathname}${search}\nhost: ${host}\ndate: ${now.toUTCString()}` const headers = { host, @@ -71,12 +71,13 @@ module.exports.fn = async (data, opts) => { accept: 'application/activity+json' } - if (jsEnv.isNode) { + if (isConfigured && jsEnv.isNode) { // Generate the signature + const signedString = `(request-target): get ${pathname}${search}\nhost: ${host}\ndate: ${now.toUTCString()}` const sign = crypto.createSign('SHA256') sign.write(signedString) sign.end() - const signatureSig = sign.sign(opts.claims.activitypub.privateKey, 'base64') + const signatureSig = sign.sign(opts.claims.activitypub.privateKey.replace(/\\n/g, '\n'), 'base64') headers.signature = `keyId="${opts.claims.activitypub.url}#main-key",headers="(request-target) host date",signature="${signatureSig}",algorithm="rsa-sha256"` } From 37fe998fd8f082234236fbf0ca175da445864fe0 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Fri, 14 Oct 2022 13:04:42 +0200 Subject: [PATCH 7/9] Do not react to lack of setup --- src/fetcher/activitypub.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fetcher/activitypub.js b/src/fetcher/activitypub.js index 89eff8d..d5173b3 100644 --- a/src/fetcher/activitypub.js +++ b/src/fetcher/activitypub.js @@ -58,9 +58,7 @@ module.exports.fn = async (data, opts) => { try { validator.isURL(opts.claims.activitypub.url) isConfigured = true - } catch (err) { - console.log(`ActivityPub fetcher was not set up (${err.message})`) - } + } catch (_) {} const now = new Date() const { host, pathname, search } = new URL(data.url) From 516896c63129b2a5fe9f901196dccad4fe4e97e9 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Mon, 24 Oct 2022 21:33:12 +0200 Subject: [PATCH 8/9] Fix header --- src/claimDefinitions/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/claimDefinitions/activitypub.js b/src/claimDefinitions/activitypub.js index c0e34f7..afdf00f 100644 --- a/src/claimDefinitions/activitypub.js +++ b/src/claimDefinitions/activitypub.js @@ -1,5 +1,5 @@ /* -Copyright 2021 Yarmo Mackenbach +Copyright 2022 Yarmo Mackenbach Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From a46311f1f8aae8a424bce4d4750856d9f3436238 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Mon, 24 Oct 2022 21:43:48 +0200 Subject: [PATCH 9/9] Fix header --- src/fetcher/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fetcher/activitypub.js b/src/fetcher/activitypub.js index d5173b3..655a0f0 100644 --- a/src/fetcher/activitypub.js +++ b/src/fetcher/activitypub.js @@ -1,5 +1,5 @@ /* -Copyright 2021 Yarmo Mackenbach +Copyright 2022 Yarmo Mackenbach Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.