diff --git a/README.md b/README.md index 45bbc0b..7574eb9 100644 --- a/README.md +++ b/README.md @@ -135,11 +135,11 @@ In order to add a new document in `DocumentManager`, the following steps should 3. Add the document to the `DocumentManager` using the `addDocument` method. In order to use with the `addDocument` method, document's data must be in CBOR bytes that has the IssuerSigned structure -according to ISO 23220-4 : +according to ISO 23220-4 __*__ : ```cddl IssuerSigned = { - "nameSpaces" : IssuerNameSpaces, ; Returned data elements + ?"nameSpaces" : IssuerNameSpaces, ; Returned data elements "issuerAuth" : IssuerAuth ; Contains the mobile security object (MSO) for issuer data authentication } IssuerNameSpaces = { ; Returned data elements for each namespace @@ -155,6 +155,8 @@ IssuerSignedItem = { IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes ``` +__*__**Important note**: Currently, the library does not support IssuerSigned structure without the `nameSpaces` field. + See the code below for an example of how to add a new document in `DocumentManager`: ```kotlin diff --git a/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager-impl/add-document.md b/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager-impl/add-document.md index 3a6ca21..d6a80a8 100644 --- a/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager-impl/add-document.md +++ b/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager-impl/add-document.md @@ -11,7 +11,7 @@ Expected data format is CBOR. The CBOR data must be in the following structure: ```cddl IssuerSigned = { - "nameSpaces" : IssuerNameSpaces, ; Returned data elements + ?"nameSpaces" : IssuerNameSpaces, ; Returned data elements "issuerAuth" : IssuerAuth ; Contains the mobile security object (MSO) for issuer data authentication } IssuerNameSpaces = { ; Returned data elements for each namespace @@ -27,6 +27,8 @@ IssuerSignedItem = { IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes ``` +**Important** Currently `nameSpaces` field should exist and must not be empty. + The document is added in the storage and can be retrieved using the DocumentManager::getDocumentById method. #### Return diff --git a/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager/add-document.md b/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager/add-document.md index 234879f..3f1d2e1 100644 --- a/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager/add-document.md +++ b/docs/document-manager/eu.europa.ec.eudi.wallet.document/-document-manager/add-document.md @@ -11,7 +11,7 @@ Expected data format is CBOR. The CBOR data must be in the following structure: ```cddl IssuerSigned = { - "nameSpaces" : IssuerNameSpaces, ; Returned data elements + ?"nameSpaces" : IssuerNameSpaces, ; Returned data elements "issuerAuth" : IssuerAuth ; Contains the mobile security object (MSO) for issuer data authentication } IssuerNameSpaces = { ; Returned data elements for each namespace @@ -27,6 +27,8 @@ IssuerSignedItem = { IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes ``` +**Important** Currently `nameSpaces` field should exist and must not be empty. + The document is added in the storage and can be retrieved using the DocumentManager::getDocumentById method. #### Return diff --git a/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/DocumentManager.kt b/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/DocumentManager.kt index 75fbc2a..0794e26 100644 --- a/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/DocumentManager.kt +++ b/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/DocumentManager.kt @@ -83,7 +83,7 @@ interface DocumentManager { * * ```cddl * IssuerSigned = { - * "nameSpaces" : IssuerNameSpaces, ; Returned data elements + * ?"nameSpaces" : IssuerNameSpaces, ; Returned data elements * "issuerAuth" : IssuerAuth ; Contains the mobile security object (MSO) for issuer data authentication * } * IssuerNameSpaces = { ; Returned data elements for each namespace @@ -99,6 +99,8 @@ interface DocumentManager { * IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes * ``` * + * **Important** Currently `nameSpaces` field should exist and must not be empty. + * * The document is added in the storage and can be retrieved using the * [DocumentManager::getDocumentById] method. * diff --git a/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/internal/SampleDataUtil.kt b/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/internal/SampleDataUtil.kt index da4f070..81ec82a 100644 --- a/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/internal/SampleDataUtil.kt +++ b/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/internal/SampleDataUtil.kt @@ -15,11 +15,19 @@ */ package eu.europa.ec.eudi.wallet.document.internal +import COSE.AlgorithmID.ECDSA_256 +import COSE.HeaderKeys.Algorithm import COSE.OneKey +import COSE.Sign1Message +import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator +import com.android.identity.util.Timestamp +import com.upokecenter.cbor.CBORObject import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.util.io.pem.PemReader import java.security.KeyFactory +import java.security.MessageDigest import java.security.PrivateKey +import java.security.PublicKey import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.security.spec.PKCS8EncodedKeySpec @@ -72,3 +80,56 @@ internal val issuerCertificate: X509Certificate = PemReader(SAMPLE_ISSUER_DS.rea @get:JvmSynthetic internal val PrivateKey.oneKey get() = OneKey(null, this) + +@JvmSynthetic +internal fun generateMso( + digestAlg: String, + docType: String, + authKey: PublicKey, + nameSpaces: CBORObject, +) = + MobileSecurityObjectGenerator(digestAlg, docType, authKey) + .apply { + val now = Timestamp.now().toEpochMilli() + val signed = Timestamp.ofEpochMilli(now) + val validFrom = Timestamp.ofEpochMilli(now) + val validUntil = Timestamp.ofEpochMilli(now + 1000L * 60L * 60L * 24L * 365L) + setValidityInfo(signed, validFrom, validUntil, null) + + val digestIds = nameSpaces.entries.associate { (nameSpace, issuerSignedItems) -> + nameSpace.AsString() to calculateDigests(digestAlg, issuerSignedItems) + } + digestIds.forEach { (nameSpace, digestIds) -> + addDigestIdsForNamespace(nameSpace, digestIds) + } + } + .generate() + +@JvmSynthetic +internal fun calculateDigests(digestAlg: String, issuerSignedItems: CBORObject): Map { + return issuerSignedItems.values.associate { issuerSignedItemBytes -> + val issuerSignedItem = issuerSignedItemBytes.getEmbeddedCBORObject() + val digest = MessageDigest.getInstance(digestAlg) + .digest(issuerSignedItemBytes.EncodeToBytes()) + issuerSignedItem["digestID"].AsInt32().toLong() to digest + } +} + +@JvmSynthetic +internal fun signMso(mso: ByteArray) = Sign1Message(false, true).apply { + protectedAttributes.Add(Algorithm.AsCBOR(), ECDSA_256.AsCBOR()) + unprotectedAttributes.Add(33L, issuerCertificate.encoded) + SetContent(mso.withTag24()) + sign(issuerPrivateKey.oneKey) +}.EncodeToCBORObject() + +@JvmSynthetic +internal fun generateData( + issuerNameSpaces: CBORObject, + issuerAuth: CBORObject, +): ByteArray { + return mapOf( + "nameSpaces" to issuerNameSpaces, + "issuerAuth" to issuerAuth, + ).let { CBORObject.FromObject(it).EncodeToBytes() } +} diff --git a/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/sample/SampleDocumentManagerImpl.kt b/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/sample/SampleDocumentManagerImpl.kt index 7531a4c..2008485 100644 --- a/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/sample/SampleDocumentManagerImpl.kt +++ b/document-manager/src/main/java/eu/europa/ec/eudi/wallet/document/sample/SampleDocumentManagerImpl.kt @@ -15,19 +15,12 @@ */ package eu.europa.ec.eudi.wallet.document.sample -import COSE.AlgorithmID.ECDSA_256 -import COSE.HeaderKeys.Algorithm -import COSE.Sign1Message import android.content.Context -import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator -import com.android.identity.util.Timestamp import com.upokecenter.cbor.CBORObject import eu.europa.ec.eudi.wallet.document.AddDocumentResult import eu.europa.ec.eudi.wallet.document.CreateIssuanceRequestResult import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.document.internal.* -import java.security.MessageDigest -import java.security.PublicKey /** * A [SampleDocumentManager] implementation that composes a [DocumentManager] and provides methods to load sample data. @@ -72,7 +65,7 @@ class SampleDocumentManagerImpl( request.name = context.docTypeName(docType) ?: docType val authKey = request.publicKey - val mso = generateMso(docType, authKey, nameSpaces) + val mso = generateMso(DIGEST_ALG, docType, authKey, nameSpaces) val issuerAuth = signMso(mso) val data = generateData(nameSpaces, issuerAuth) @@ -94,51 +87,5 @@ class SampleDocumentManagerImpl( private companion object { private const val DIGEST_ALG = "SHA-256" - - private fun generateMso( - docType: String, - authKey: PublicKey, - nameSpaces: CBORObject, - ) = - MobileSecurityObjectGenerator(DIGEST_ALG, docType, authKey) - .apply { - val now = Timestamp.now().toEpochMilli() - val signed = Timestamp.ofEpochMilli(now) - val validFrom = Timestamp.ofEpochMilli(now) - val validUntil = Timestamp.ofEpochMilli(now + 1000L * 60L * 60L * 24L * 365L) - setValidityInfo(signed, validFrom, validUntil, null) - - nameSpaces.entries.forEach { (nameSpace, issuerSignedItems) -> - val digestIds = calculateDigests(issuerSignedItems) - addDigestIdsForNamespace(nameSpace.AsString(), digestIds) - } - } - .generate() - - private fun calculateDigests(issuerSignedItems: CBORObject): Map { - return issuerSignedItems.values.associate { issuerSignedItemBytes -> - val issuerSignedItem = issuerSignedItemBytes.getEmbeddedCBORObject() - val digest = MessageDigest.getInstance(DIGEST_ALG) - .digest(issuerSignedItemBytes.EncodeToBytes()) - issuerSignedItem["digestID"].AsInt32().toLong() to digest - } - } - - private fun signMso(mso: ByteArray) = Sign1Message(false, true).apply { - protectedAttributes.Add(Algorithm.AsCBOR(), ECDSA_256.AsCBOR()) - unprotectedAttributes.Add(33L, issuerCertificate.encoded) - SetContent(mso.withTag24()) - sign(issuerPrivateKey.oneKey) - }.EncodeToCBORObject() - - private fun generateData( - issuerNameSpaces: CBORObject, - issuerAuth: CBORObject, - ): ByteArray { - return mapOf( - "nameSpaces" to issuerNameSpaces, - "issuerAuth" to issuerAuth, - ).let { CBORObject.FromObject(it).EncodeToBytes() } - } } }