From bff79686fdf4cc05610cdcd9f6a40d658fae854a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 1 Nov 2021 15:52:26 +0100 Subject: [PATCH 1/5] Add missing properties in asRegistrationTestData --- .../src/test/scala/com/yubico/webauthn/test/RealExamples.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala index 11b36a4ab..f69873ce5 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala @@ -110,6 +110,8 @@ object RealExamples { attestationObject = attestation.attestationObjectBytes, clientDataJson = attestation.clientData, privateKey = None, + rpId = rp, + userId = user, assertion = Some( AssertionTestData( request = AssertionRequest @@ -120,6 +122,7 @@ object RealExamples { .challenge(assertion.collectedClientData.getChallenge) .build() ) + .username(user.getName) .build(), response = assertion.credential, ) From 32b26ef7359dee7bc63b007215020c08857a3edc Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 29 Oct 2021 17:12:57 +0200 Subject: [PATCH 2/5] Test that assertion verification works with U2F-formatted public keys --- .../webauthn/RelyingPartyAssertionSpec.scala | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index 0e3f30453..cacceeb5d 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -52,6 +52,7 @@ import com.yubico.webauthn.data.UserVerificationRequirement import com.yubico.webauthn.exception.InvalidSignatureCountException import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.test.Helpers +import com.yubico.webauthn.test.RealExamples import com.yubico.webauthn.test.Util.toStepWithUtilities import org.junit.runner.RunWith import org.scalacheck.Gen @@ -1946,6 +1947,55 @@ class RelyingPartyAssertionSpec test(RegistrationTestData.Packed.SelfAttestationRs1) } } + + it("a U2F-formatted public key.") { + val testData = RealExamples.YubiKeyNeo.asRegistrationTestData + val x = ByteArray.fromHex( + "39C94FBBDDC694A925E6F8657C66916CFE84CD0222EDFCF281B21F5CDC347923" + ) + val y = ByteArray.fromHex( + "D6B0D2021CFE1724A6FE81E3568C4FFAE339298216A30AFC18C0B975F2E2A891" + ) + val u2fPubkey = ByteArray.fromHex("04").concat(x).concat(y) + + val cred1 = RegisteredCredential + .builder() + .credentialId(testData.assertion.get.response.getId) + .userHandle(testData.userId.getId) + .publicKeyCose(u2fPubkey) + .signatureCount(0) + .build() + + val cred2 = RegisteredCredential + .builder() + .credentialId(testData.assertion.get.response.getId) + .userHandle(testData.userId.getId) + .publicKeyCose(u2fPubkey) + .signatureCount(0) + .build() + + for { cred <- List(cred1, cred2) } { + val rp = RelyingParty + .builder() + .identity(testData.rpId) + .credentialRepository( + Helpers.CredentialRepository.withUser(testData.userId, cred) + ) + .build() + + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(testData.assertion.get.request) + .response(testData.assertion.get.response) + .build() + ) + + result.isSuccess should be(true) + result.getUserHandle should equal(testData.userId.getId) + result.getCredentialId should equal(testData.response.getId) + } + } } describe("The default RelyingParty settings") { From a692324830c70d3623545ad0244101f0d09dccfa Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 1 Nov 2021 17:14:53 +0100 Subject: [PATCH 3/5] Add method RegisteredCredential.builder().publicKeyEs256Raw(ByteArray) --- NEWS | 9 +++ .../yubico/webauthn/RegisteredCredential.java | 74 +++++++++++++++++++ .../com/yubico/webauthn/WebAuthnCodecs.java | 24 ++++++ .../webauthn/RelyingPartyAssertionSpec.scala | 3 +- .../yubico/webauthn/WebAuthnCodecsSpec.scala | 4 +- .../yubico/webauthn/WebAuthnTestCodecs.scala | 35 +-------- 6 files changed, 113 insertions(+), 36 deletions(-) diff --git a/NEWS b/NEWS index f20ac12be..1a38fc2b5 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,12 @@ +== Version 1.12.0 (unreleased) == + +New features: + +* New method `RegisteredCredential.builder().publicKeyEs256Raw(ByteArray)`. This + is a mutually exclusive alternative to `.publicKeyCose(ByteArray)`, for easier + backwards-compatibility with U2F-formatted (Raw ANSI X9.62) public keys. + + == Version 1.11.0 == Deprecated features: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java index 0c2c787fd..ef1cfb89f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java @@ -30,6 +30,7 @@ import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.AuthenticatorData; import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.COSEAlgorithmIdentifier; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.UserIdentity; import lombok.Builder; @@ -143,12 +144,85 @@ public class Step3 { * {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required * parameter. * + *

Alternatively, the public key can be specified using the {@link + * #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format ( + * ALG_KEY_ECC_X962_RAW as specified in FIDO + * Registry §3.6.2 Public Key Representation Formats). This is mostly useful for public + * keys registered via the U2F JavaScript API. + * + * @see #publicKeyEs256Raw(ByteArray) * @see RegisteredCredentialBuilder#publicKeyCose(ByteArray) + * @see FIDO + * Registry §3.6.2 Public Key Representation Formats */ public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) { return builder.publicKeyCose(publicKeyCose); } + + /** + * Specify the credential public key in U2F format. + * + *

An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link + * COSEAlgorithmIdentifier#ES256 ES256} public key in ALG_KEY_ECC_X962_RAW + * format as specified in FIDO + * Registry §3.6.2 Public Key Representation Formats. + * + *

This is primarily intended for public keys registered via the U2F JavaScript API. If + * your application has only used the navigator.credentials.create() API to + * register credentials, you should use {@link #publicKeyCose(ByteArray)} instead. + * + * @see RegisteredCredentialBuilder#publicKeyCose(ByteArray) + */ + public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) { + return builder.publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw)); + } } } + + /** + * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. This method overwrites {@link + * #publicKeyEs256Raw(ByteArray)}. + * + *

This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature} + * in authentication assertions. + * + *

Alternatively, the public key can be specified using the {@link + * #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format ( + * ALG_KEY_ECC_X962_RAW as specified in FIDO + * Registry §3.6.2 Public Key Representation Formats). This is mostly useful for public keys + * registered via the U2F JavaScript API. + * + * @see AttestedCredentialData#getCredentialPublicKey() + * @see RegistrationResult#getPublicKeyCose() + */ + public RegisteredCredentialBuilder publicKeyCose(@NonNull ByteArray publicKeyCose) { + this.publicKeyCose = publicKeyCose; + return this; + } + + /** + * Specify the credential public key in U2F format. This method overwrites {@link + * #publicKeyCose(ByteArray)}. + * + *

An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link + * COSEAlgorithmIdentifier#ES256 ES256} public key in ALG_KEY_ECC_X962_RAW format + * as specified in FIDO + * Registry §3.6.2 Public Key Representation Formats. + * + *

This is primarily intended for public keys registered via the U2F JavaScript API. If your + * application has only used the navigator.credentials.create() API to register + * credentials, you should use {@link #publicKeyCose(ByteArray)} instead. + * + * @see RegisteredCredentialBuilder#publicKeyCose(ByteArray) + */ + public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) { + return publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw)); + } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java index 6a784759a..986061eb2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java @@ -40,6 +40,8 @@ import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; final class WebAuthnCodecs { @@ -63,6 +65,28 @@ static ByteArray ecPublicKeyToRaw(ECPublicKey key) { Bytes.concat(yPadding, Arrays.copyOfRange(y, Math.max(0, y.length - 32), y.length)))); } + static ByteArray rawEcKeyToCose(ByteArray key) { + final byte[] keyBytes = key.getBytes(); + if (!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes[0] == 0x04))) { + throw new IllegalArgumentException( + String.format( + "Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was %d bytes starting with %02x", + keyBytes.length, keyBytes[0])); + } + final int start = (keyBytes.length == 64) ? 0 : 1; + + final Map coseKey = new HashMap<>(); + coseKey.put(1L, 2L); // Key type: EC + + coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId()); + coseKey.put(-1L, 1L); // Curve: P-256 + + coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + 32)); // x + coseKey.put(-3L, Arrays.copyOfRange(keyBytes, start + 32, start + 64)); // y + + return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes()); + } + static PublicKey importCosePublicKey(ByteArray key) throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException { CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes()); diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index cacceeb5d..e525c3190 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -1962,7 +1962,7 @@ class RelyingPartyAssertionSpec .builder() .credentialId(testData.assertion.get.response.getId) .userHandle(testData.userId.getId) - .publicKeyCose(u2fPubkey) + .publicKeyEs256Raw(u2fPubkey) .signatureCount(0) .build() @@ -1972,6 +1972,7 @@ class RelyingPartyAssertionSpec .userHandle(testData.userId.getId) .publicKeyCose(u2fPubkey) .signatureCount(0) + .publicKeyEs256Raw(u2fPubkey) .build() for { cred <- List(cred1, cred2) } { diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala index 490da14ae..948109799 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala @@ -86,13 +86,13 @@ class WebAuthnCodecsSpec } - describe("The rawEcdaKeyToCose method") { + describe("The rawEcKeyToCose method") { it("outputs a value that can be imported by importCoseP256PublicKey") { forAll { originalPubkey: ECPublicKey => val rawKey = WebAuthnCodecs.ecPublicKeyToRaw(originalPubkey) - val coseKey = WebAuthnTestCodecs.rawEcdaKeyToCose(rawKey) + val coseKey = WebAuthnCodecs.rawEcKeyToCose(rawKey) val importedPubkey: ECPublicKey = WebAuthnCodecs .importCosePublicKey(coseKey) diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala index 851fb9266..92a68dd67 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala @@ -1,6 +1,7 @@ package com.yubico.webauthn import com.upokecenter.cbor.CBORObject +import com.yubico.webauthn.WebAuthnCodecs.rawEcKeyToCose import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.COSEAlgorithmIdentifier import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey @@ -22,39 +23,7 @@ object WebAuthnTestCodecs { def importCosePublicKey = WebAuthnCodecs.importCosePublicKey _ def ecPublicKeyToCose(key: ECPublicKey): ByteArray = - rawEcdaKeyToCose(ecPublicKeyToRaw(key)) - - def rawEcdaKeyToCose(key: ByteArray): ByteArray = { - val keyBytes = key.getBytes - if ( - !(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes(0) == 0x04)) - ) { - throw new IllegalArgumentException( - s"Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was ${keyBytes.length} bytes starting with ${keyBytes(0)}" - ) - } - val start: Int = - if (keyBytes.length == 64) 0 - else 1 - - val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any] - coseKey.put(1L, 2L) // Key type: EC - - coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId) - coseKey.put(-1L, 1L) // Curve: P-256 - - coseKey.put( - -2L, - java.util.Arrays.copyOfRange(keyBytes, start, start + 32), - ) // x - - coseKey.put( - -3L, - java.util.Arrays.copyOfRange(keyBytes, start + 32, start + 64), - ) // y - - new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes) - } + rawEcKeyToCose(ecPublicKeyToRaw(key)) def publicKeyToCose(key: PublicKey): ByteArray = { key match { From 945fed5136bbc243f14bda49c04b1ca3d13bf3ec Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 1 Nov 2021 17:18:21 +0100 Subject: [PATCH 4/5] Recommend RegistrationResult.getPublicKeyCose() as argument to RegisteredCredential --- .../main/java/com/yubico/webauthn/RegisteredCredential.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java index ef1cfb89f..0ba783bf0 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java @@ -144,6 +144,9 @@ public class Step3 { * {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required * parameter. * + *

The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable + * argument for this method. + * *

Alternatively, the public key can be specified using the {@link * #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format ( * ALG_KEY_ECC_X962_RAW as specified in RFC 8152. This method overwrites {@link * #publicKeyEs256Raw(ByteArray)}. * + *

The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable argument + * for this method. + * *

This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature} * in authentication assertions. * From 0762b128b2546533676f9df13c88695c2c727fe9 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 4 Nov 2021 20:45:21 +0100 Subject: [PATCH 5/5] Add U2F migration guide --- NEWS | 1 + README | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/NEWS b/NEWS index 1a38fc2b5..1a2858ed7 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ New features: * New method `RegisteredCredential.builder().publicKeyEs256Raw(ByteArray)`. This is a mutually exclusive alternative to `.publicKeyCose(ByteArray)`, for easier backwards-compatibility with U2F-formatted (Raw ANSI X9.62) public keys. +* "Migrating from U2F" section added to project README == Version 1.11.0 == diff --git a/README b/README index 452c3b42d..f34605ea5 100644 --- a/README +++ b/README @@ -462,6 +462,70 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration( ---------- +== Migrating from U2F + +This section is only relevant for applications that have user credentials registered via the +link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html[U2F JavaScript API]. +New WebAuthn deployments can skip this section. + +The WebAuthn API is backwards-compatible with U2F authenticators, +and credentials registered via the U2F API will continue to work with the WebAuthn API with the right settings. + +To migrate to using the WebAuthn API, you need to do the following: + +* Follow the link:#getting-started[Getting started] guide above to set up WebAuthn support in general. ++ +Note that unlike a U2F AppID, the WebAuthn link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/RelyingPartyIdentity.RelyingPartyIdentityBuilder.html#id(java.lang.String)[RP ID] +consists of only the domain name of the AppID. +WebAuthn does not support link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html[U2F Trusted Facet Lists]. + +* Set the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#appId(com.yubico.webauthn.extension.appid.AppId)[`appId()`] + setting on your `RelyingParty` instance. + The argument to the `appid()` setting should be the same as you used for the `appId` argument to the + link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[U2F `register` and `sign` functions]. ++ +This will enable the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-appid-extension[`appid`] +and link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-appid-exclude-extension[`appidExclude`] +extensions and configure the `RelyingParty` to accept the given AppId when verifying authenticator signatures. + +* Generate a link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle[user handle] for each existing user + and store it in their account, + or decide on a method for deriving one deterministically from existing user attributes. + For example, if your user records are assigned UUIDs, you can use that UUID as the user handle. + You SHOULD NOT use a plain username or e-mail address, or hash of either, as the user handle - + for more on this, see the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-user-handle-privacy[User Handle Contents] + privacy consideration. + +* When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, + use the U2F key handle as the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#credentialId(com.yubico.webauthn.data.ByteArray)[credential ID]. + If you store key handles base64 encoded, you should decode them using + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/ByteArray.html#fromBase64(java.lang.String)[`ByteArray.fromBase64`] + or + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/ByteArray.html#fromBase64Url(java.lang.String)[`ByteArray.fromBase64Url`] + as appropriate before passing them to the `RegisteredCredential`. + +* When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, + use the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyEs256Raw(com.yubico.webauthn.data.ByteArray)[`publicKeyEs256Raw()`] + method instead of link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyCose(com.yubico.webauthn.data.ByteArray)[`publicKeyCose()`] + to set the credential public key. + +* Replace calls to the U2F + link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[`register`] + method with calls to `navigator.credentials.create()` as described in link:#getting-started[Getting started]. + +* Replace calls to the U2F + link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[`sign`] + method with calls to `navigator.credentials.get()` as described in link:#getting-started[Getting started]. + +Existing U2F credentials should now work with the WebAuthn API. + +Note that new credentials registered on U2F authenticators via the WebAuthn API +are NOT backwards compatible with the U2F JavaScript API. + + == Architecture The library tries to place as few requirements on the overall application