Skip to content

Commit

Permalink
feat: add support for MAC device authentication in mDL
Browse files Browse the repository at this point in the history
Signed-off-by: Bjorn Molin <[email protected]>
  • Loading branch information
Razumain authored and bjornmolin committed Feb 17, 2025
1 parent 2ff2b30 commit 23b8e9a
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 58 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SPDX-License-Identifier: CC0-1.0

<groupId>se.digg.wallet</groupId>
<artifactId>eudiw-wallet-token-lib</artifactId>
<version>0.0.7-SNAPSHOT</version>
<version>0.0.8-SNAPSHOT</version>
<name>EUDI Wallet -- Token Library</name>
<description>Library for handling data types in the EUDI Wallet PoC project.</description>
<url>https://github.com/diggsweden/eudiw-wallet-token-lib</url>
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/se/digg/wallet/datatypes/mdl/data/CBORUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@
import com.upokecenter.cbor.CBORType;
import com.upokecenter.numbers.EInteger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
Expand All @@ -22,12 +30,17 @@
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.crypto.KeyAgreement;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;
import se.digg.cose.AlgorithmID;
import se.digg.cose.Attribute;
import se.digg.cose.COSEKey;
import se.digg.cose.CoseException;
import se.digg.cose.HeaderKeys;
import se.digg.cose.MAC0COSEObject;
import se.digg.cose.Sign1COSEObject;

/**
Expand Down Expand Up @@ -242,4 +255,107 @@ public static Sign1COSEObject sign(
coseSignature.sign(key);
return coseSignature;
}

public static MAC0COSEObject deviceComputedMac(byte[] deviceAuthenticationBytes, PrivateKey privateKey, PublicKey publicKey) throws GeneralSecurityException, CoseException {
byte[] sharedSecret = deriveSharedSecret(privateKey, publicKey);
MAC0COSEObject mac0COSEObject = new MAC0COSEObject();
mac0COSEObject.addAttribute(
HeaderKeys.Algorithm,
AlgorithmID.HMAC_SHA_256.AsCBOR(),
Attribute.PROTECTED);
mac0COSEObject.SetContent(deviceAuthenticationBytes);
byte[] macKey = deriveEMacKey(sharedSecret, deviceAuthenticationBytes);
mac0COSEObject.Create(macKey);
return mac0COSEObject;
}

/**
* Derives the EMacKey using the HKDF function as defined in RFC 5869.
*
* @param zab input keying material (IKM) as byte array.
* @param sessionTranscriptBytes session transcript bytes to be hashed with SHA-256 as salt
* @return A 32-byte EMacKey derived from HKDF.
*/
public static byte[] deriveEMacKey(byte[] zab, byte[] sessionTranscriptBytes) {
// Step 1: Create salt as SHA-256(sessionTranscriptBytes)
SHA256Digest sha256Digest = new SHA256Digest(); // Use BouncyCastle's Digest
byte[] salt = hash(sha256Digest, sessionTranscriptBytes);

// Step 2: Define the info parameter as "EMacKey" encoded in UTF-8
byte[] info = "EMacKey".getBytes(StandardCharsets.UTF_8);

// Step 3: Setup HKDF parameters with SHA-256 hash, IKM, salt, and info.
HKDFParameters hkdfParameters = new HKDFParameters(zab, salt, info);

// Step 4: Create the HKDF generator
HKDFBytesGenerator hkdfGenerator = new HKDFBytesGenerator(sha256Digest);

// Step 5: Initialize the generator with our parameters
hkdfGenerator.init(hkdfParameters);

// Step 6: Generate the key (L = 32 bytes)
byte[] eMacKey = new byte[32];
hkdfGenerator.generateBytes(eMacKey, 0, eMacKey.length);

// Return the derived EMacKey
return eMacKey;
}

/**
* Helper method to hash input data using a given Digest.
*
* @param digest The SHA-256 Digest instance.
* @param input The input data to hash.
* @return The hashed result as a byte array.
*/
private static byte[] hash(SHA256Digest digest, byte[] input) {
digest.reset();
digest.update(input, 0, input.length);
byte[] output = new byte[digest.getDigestSize()];
digest.doFinal(output, 0);
return output;
}



/**
* Derives a shared secret using Diffie-Hellman (DH) key derivation.
*
* @param privateKey The private key (either RSA or EC).
* @param publicKey The public key (must be of the same type as the private key).
* @return A byte array representing the derived shared secret.
* @throws IllegalArgumentException if the keys are not of the same type or unsupported key types are provided.
* @throws GeneralSecurityException if key agreement fails.
*/
public static byte[] deriveSharedSecret(PrivateKey privateKey, PublicKey publicKey)
throws GeneralSecurityException {
// Ensure the key types match
if (privateKey instanceof RSAPrivateKey || publicKey instanceof RSAPublicKey) {
throw new IllegalArgumentException("RSA keys cannot be used for key agreement (DH). Use EC keys instead.");
} else if (privateKey instanceof ECPrivateKey && publicKey instanceof ECPublicKey) {
return deriveECSharedSecret((ECPrivateKey) privateKey, (ECPublicKey) publicKey);
} else {
throw new IllegalArgumentException("Key types do not match or are unsupported. Use RSA or EC keys.");
}
}

/**
* Derives a shared secret using EC keys.
*
* @param privateKey The EC private key.
* @param publicKey The EC public key.
* @return A byte array representing the derived shared secret.
* @throws GeneralSecurityException if key agreement fails.
*/
private static byte[] deriveECSharedSecret(ECPrivateKey privateKey, ECPublicKey publicKey)
throws GeneralSecurityException {
// Create EC Key Agreement
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
keyAgreement.init(privateKey);
keyAgreement.doPhase(publicKey, true);

// Generate shared secret
return keyAgreement.generateSecret();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class DeviceResponse {
/**
* Constructor for the DeviceResponse class.
* Initializes a new instance of the DeviceResponse with the specified parameters.
* including signature device authentication
*
* @param docType the document type associated with the response.
* @param issuerSigned the issuer-signed data associated with the device response.
Expand All @@ -55,6 +56,29 @@ public DeviceResponse(
this.deviceMac = null;
}

/**
* Constructor for the DeviceResponse class.
* Initializes a new instance of the DeviceResponse with the specified parameters
* including MAC device authentication.
*
* @param deviceMac the byte array representing the device MAC.
* @param docType the document type associated with the response.
* @param issuerSigned the issuer-signed data associated with the device response.
*/
public DeviceResponse(
byte[] deviceMac,
String docType,
IssuerSigned issuerSigned
) {
this.issuerSigned = issuerSigned;
this.docType = docType;
this.version = "1.0";
this.status = 0;
this.deviceNameSpaces = CBORObject.NewMap();
this.deviceMac = deviceMac;
this.deviceSignature = null;
}

/** Status code. Default 0 for successful responses */
private final int status;
/** DocType for the response document */
Expand All @@ -67,7 +91,7 @@ public DeviceResponse(
private final CBORObject deviceNameSpaces;
/** The bytes of the device signature */
private final byte[] deviceSignature;
/** The bytes of a wallet provided MAC (Currently not supported) */
/** The bytes of a wallet provided MAC */
private final byte[] deviceMac;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package se.digg.wallet.datatypes.mdl.data;

import java.security.PublicKey;
import java.util.List;
import java.util.Map;
import lombok.Getter;
Expand All @@ -26,6 +27,10 @@ public class MdlPresentationInput
private String mdocGeneratedNonce;
/** The response URL where the presentation response is delivered */
private String responseUri;
/** Client MAC key derivation key **/
private PublicKey clientPublicKey;
/** Set to true to use MAC device authentication. If set to true clientPublicKey MUST be set */
boolean macDeviceAuthentication = false;

/**
* Creates and returns a new instance of the {@link MdlPresentationInputBuilder} class.
Expand Down Expand Up @@ -135,6 +140,32 @@ public MdlPresentationInputBuilder algorithm(
return this;
}

/**
* Sets the optional client public key to be used in the {@code MdlPresentationInput} object being built.
* If this key is provided, this will enable derivation of a MAC key to provide a MAC device key proof.
*
* @param clientPublicKey the {@code PublicKey} instance representing the client's public key
* @return the {@code MdlPresentationInputBuilder} instance for method chaining
*/
public MdlPresentationInputBuilder clientPublicKey(PublicKey clientPublicKey) {
mdlPresentationInput.clientPublicKey = clientPublicKey;
return this;
}

/**
* Sets whether MAC (Message Authentication Code) device authentication should be enabled
* in the {@code MdlPresentationInput} object being built.
*
* @param macDeviceAuthentication a boolean value indicating whether MAC device authentication
* should be enabled. If set to true, MAC device authentication
* will be used; otherwise, device signature will be applied (default false).
* @return the {@code MdlPresentationInputBuilder} instance for method chaining.
*/
public MdlPresentationInputBuilder macDeviceAuthentication(boolean macDeviceAuthentication) {
mdlPresentationInput.macDeviceAuthentication = macDeviceAuthentication;
return this;
}

/**
* Builds and returns the fully constructed {@code MdlPresentationInput} object.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package se.digg.wallet.datatypes.mdl.data;

import java.security.PrivateKey;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -46,4 +47,6 @@ public MdlPresentationValidationInput(
private String responseUri;
/** The wallet generated nonce included as the apu header parameter in the presentation response JWT */
private String mdocGeneratedNonce;
/** Optional private key for MAC validation */
private PrivateKey clientPrivateKey;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.bouncycastle.util.encoders.Hex;
import se.digg.cose.COSEObjectTag;
import se.digg.cose.CoseException;
import se.digg.cose.MAC0COSEObject;
import se.digg.cose.Sign1COSEObject;
import se.digg.wallet.datatypes.common.PresentationValidationInput;
import se.digg.wallet.datatypes.common.PresentationValidator;
Expand Down Expand Up @@ -124,23 +125,12 @@ public MdlPresentationValidationResult validatePresentation(
new MdlIssuerSignedValidator(timeSkew);
MdlIssuerSignedValidationResult issuerSignedValidationResult =
issuerSignedValidator.validateToken(issuerSignedBytes, trustedKeys);
// For now only accept device signatures
if (deviceResponse.getDeviceMac() != null) {
// TODO support device Mac authentication
log.debug(
"This presentation has a device mac. This is not supported yet and ignored"
);
}
if (deviceResponse.getDeviceSignature() == null) {
// As we only support device signature. One must be available
// Ensure that device MAC or signature is present
if (deviceResponse.getDeviceMac() == null && deviceResponse.getDeviceSignature() == null) {
throw new TokenValidationException(
"Token presentation must have a device signature"
"Token presentation must name device mac or device signature"
);
}
// Get the detached device signature
CBORObject deviceSignatureObject = CBORObject.DecodeFromBytes(
deviceResponse.getDeviceSignature()
);
// Reconstruct the detached data
DeviceAuthentication deviceAuthentication = new DeviceAuthentication(
deviceResponse.getDocType(),
Expand All @@ -151,32 +141,73 @@ public MdlPresentationValidationResult validatePresentation(
input.getMdocGeneratedNonce()
)
);
// Insert the detached data as payload
deviceSignatureObject.set(
2,
CBORObject.FromByteArray(
deviceAuthentication.getDeviceAuthenticationBytes()
)
);
// Create the signed object with the restored payload
Sign1COSEObject sign1COSEObject =
(Sign1COSEObject) Sign1COSEObject.DecodeFromBytes(
deviceSignatureObject.EncodeToBytes(),
COSEObjectTag.Sign1
);
// Get the device key
// Get the wallet device key
MobileSecurityObject.DeviceKeyInfo deviceKeyInfo =
issuerSignedValidationResult.getMso().getDeviceKeyInfo();
// Validate signature against device key
boolean deviceSignatureValid = sign1COSEObject.validate(
deviceKeyInfo.getDeviceKey()
);
if (!deviceSignatureValid) {
// Device signature was invalid
throw new TokenValidationException("Device signature is invalid");

// Validate MAC if present
if (deviceResponse.getDeviceMac() != null) {
if (input.getClientPrivateKey() == null) {
throw new TokenValidationException(
"Client private key must be provided for MAC validation"
);
}
CBORObject deviceMacObject = CBORObject.DecodeFromBytes(
deviceResponse.getDeviceMac()
);
// Insert the detached data as payload
deviceMacObject.set(
2,
CBORObject.FromByteArray(
deviceAuthentication.getDeviceAuthenticationBytes()
)
);
MAC0COSEObject mac0COSEObject =
(MAC0COSEObject) MAC0COSEObject.DecodeFromBytes(
deviceMacObject.EncodeToBytes(),
COSEObjectTag.MAC0
);
boolean validMac = mac0COSEObject.Validate(CBORUtils.deriveEMacKey(
CBORUtils.deriveSharedSecret(input.getClientPrivateKey(), deviceKeyInfo.getDeviceKey().AsPublicKey()),
deviceAuthentication.getDeviceAuthenticationBytes()
));
if (!validMac) {
// Device signature was invalid
throw new TokenValidationException("Device signature is invalid");
}
log.debug("Device MAC is valid");
}
// Validate device signature if present
if (deviceResponse.getDeviceSignature() != null) {
// Get the detached device signature
CBORObject deviceSignatureObject = CBORObject.DecodeFromBytes(
deviceResponse.getDeviceSignature()
);
// Insert the detached data as payload
deviceSignatureObject.set(
2,
CBORObject.FromByteArray(
deviceAuthentication.getDeviceAuthenticationBytes()
)
);
// Create the signed object with the restored payload
Sign1COSEObject sign1COSEObject =
(Sign1COSEObject) Sign1COSEObject.DecodeFromBytes(
deviceSignatureObject.EncodeToBytes(),
COSEObjectTag.Sign1
);
// Validate signature against device key
boolean deviceSignatureValid = sign1COSEObject.validate(
deviceKeyInfo.getDeviceKey()
);
if (!deviceSignatureValid) {
// Device signature was invalid
throw new TokenValidationException("Device signature is invalid");
}
// Signature is valid. Provide result data
log.debug("Device signature is valid");
}
// Signature is valid. Provide result data
log.debug("Device signature is valid");
// Retrieve disclosed signatures
Map<TokenAttributeType, Object> disclosedAttributes =
getDisclosedAttributes(issuerSigned.getNameSpaces());
issuerSignedValidationResult.setPresentationRequestNonce(
Expand Down
Loading

0 comments on commit 23b8e9a

Please sign in to comment.