From 520525bde3549608268de069c9d93e1232664f5d Mon Sep 17 00:00:00 2001 From: Warren Gallagher Date: Mon, 27 Oct 2025 15:20:47 -0400 Subject: [PATCH 1/6] feat: add Azure Key Vault HSM signing support for mDL issuance Add Azure Key Vault integration for hardware-backed cryptographic signing of mobile driver's licenses (mDL) compliant with ISO 18013-5. Key changes: - Introduce Signer abstraction interface for pluggable signing implementations - Implement AzureKeyVaultSigner with @azure/keyvault-keys and @azure/identity - Refactor existing signing logic into LocalKeySigner for backward compatibility - Add comprehensive test suite with certificate chain validation - Support ES256, ES384, ES512, and RSA signature algorithms - Document EdDSA/Ed25519 limitation (not supported by Azure Key Vault HSM) - Include setup guide and integration documentation All 115 tests passing with 100% backward compatibility maintained. --- .gitignore | 2 + AZURE_KEY_VAULT_EXAMPLE.md | 304 +++++++ AZURE_KEY_VAULT_INTEGRATION.md | 204 +++++ __tests__/issuing/azureKeyVault.tests.ts | 429 ++++++++++ package-lock.json | 970 ++++++++++++++++++++++- package.json | 2 + sample-azure-env.sh | 57 ++ src/index.ts | 1 + src/mdoc/model/Document.ts | 65 +- src/mdoc/model/IssuerAuth.ts | 152 +++- src/mdoc/signing/AzureKeyVaultSigner.ts | 210 +++++ src/mdoc/signing/LocalKeySigner.ts | 68 ++ src/mdoc/signing/Signer.ts | 27 + src/mdoc/signing/index.ts | 3 + 14 files changed, 2458 insertions(+), 36 deletions(-) create mode 100644 AZURE_KEY_VAULT_EXAMPLE.md create mode 100644 AZURE_KEY_VAULT_INTEGRATION.md create mode 100644 __tests__/issuing/azureKeyVault.tests.ts create mode 100755 sample-azure-env.sh create mode 100644 src/mdoc/signing/AzureKeyVaultSigner.ts create mode 100644 src/mdoc/signing/LocalKeySigner.ts create mode 100644 src/mdoc/signing/Signer.ts create mode 100644 src/mdoc/signing/index.ts diff --git a/.gitignore b/.gitignore index e0b3772..983c0ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /tmp /coverage .DS_Store +azure-env.sh +.claude/ \ No newline at end of file diff --git a/AZURE_KEY_VAULT_EXAMPLE.md b/AZURE_KEY_VAULT_EXAMPLE.md new file mode 100644 index 0000000..5bd928e --- /dev/null +++ b/AZURE_KEY_VAULT_EXAMPLE.md @@ -0,0 +1,304 @@ +# Azure Key Vault Signing Example + +This example demonstrates how to use Azure Key Vault to sign mDL/MDOC documents using the +`AzureKeyVaultSigner`. + +## Prerequisites + +1. An Azure Key Vault instance +2. An EC (Elliptic Curve) key stored in Azure Key Vault (ES256, ES384, or ES512) +3. Appropriate Azure credentials configured (e.g., via Azure CLI, environment variables, or + managed identity) + +## Installation + +```bash +npm install @auth0/mdl @azure/keyvault-keys @azure/identity +``` + +## Basic Usage + +### Using Azure Key Vault for Signing + +```typescript +import { Document, AzureKeyVaultSigner } from '@auth0/mdl'; + +// Create an Azure Key Vault signer +const signer = new AzureKeyVaultSigner({ + keyVaultUrl: 'https://my-vault.vault.azure.net', + keyName: 'my-signing-key', + keyVersion: 'latest', // Optional, defaults to latest + // credential: new DefaultAzureCredential(), // Optional, uses DefaultAzureCredential +}); + +// Create and sign a document +const doc = new Document('org.iso.18013.5.1.mDL'); + +doc.addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Doe', + given_name: 'John', + birth_date: '1990-01-01', + issue_date: '2024-01-01', + expiry_date: '2034-01-01', + issuing_country: 'US', + document_number: '123456789', +}); + +// Sign using Azure Key Vault +const signedDoc = await doc.sign({ + signer, + issuerCertificate: certificatePEM, // Your issuer certificate + alg: 'ES256', // Must match your Azure Key Vault key algorithm + kid: 'my-key-id', // Optional key ID +}); + +console.log('Document signed successfully with Azure Key Vault!'); +``` + +### Using Custom Azure Credentials + +```typescript +import { AzureKeyVaultSigner } from '@auth0/mdl'; +import { ClientSecretCredential } from '@azure/identity'; + +// Use specific credentials instead of DefaultAzureCredential +const credential = new ClientSecretCredential( + 'tenant-id', + 'client-id', + 'client-secret' +); + +const signer = new AzureKeyVaultSigner({ + keyVaultUrl: 'https://my-vault.vault.azure.net', + keyName: 'my-signing-key', + credential, +}); + +// Use the signer as shown above +``` + +### Backward Compatibility - Traditional Signing + +The traditional signing method with local keys continues to work: + +```typescript +import { Document } from '@auth0/mdl'; +import * as jose from 'jose'; + +const privateKeyJWK: jose.JWK = { + kty: 'EC', + crv: 'P-256', + x: '...', + y: '...', + d: '...', + kid: 'key-id', +}; + +const doc = new Document('org.iso.18013.5.1.mDL'); +// ... add namespaces ... + +// Traditional signing (still supported) +const signedDoc = await doc.sign({ + issuerPrivateKey: privateKeyJWK, + issuerCertificate: certificatePEM, + alg: 'ES256', +}); +``` + +## Supported Algorithms + +Azure Key Vault supports the following EC signing algorithms that work with this library: + +- **ES256** - ECDSA using P-256 and SHA-256 ✅ Supported +- **ES384** - ECDSA using P-384 and SHA-384 ✅ Supported +- **ES512** - ECDSA using P-521 and SHA-512 ✅ Supported +- **EdDSA** - Edwards-curve Digital Signature Algorithm ❌ **NOT supported by Azure Key Vault HSM** + +**Note**: Azure Key Vault HSM does not support Ed25519 keys or the EdDSA algorithm. If you +need EdDSA, you must use local key signing with `LocalKeySigner`. + +Make sure your Azure Key Vault key type matches the algorithm you're using. + +## Azure Key Vault Setup + +### Creating an EC Key in Azure Key Vault + +Using Azure CLI: + +```bash +# Create a P-256 key for ES256 +az keyvault key create \ + --vault-name my-vault \ + --name my-signing-key \ + --kty EC \ + --curve P-256 \ + --ops sign verify + +# Or create a P-384 key for ES384 +az keyvault key create \ + --vault-name my-vault \ + --name my-signing-key \ + --kty EC \ + --curve P-384 \ + --ops sign verify +``` + +### Required Permissions + +Your Azure identity needs the following permissions on the Key Vault: + +- `keys/get` - To retrieve the public key +- `keys/sign` - To perform signing operations + +Example policy assignment: + +```bash +az keyvault set-policy \ + --name my-vault \ + --object-id \ + --key-permissions get sign +``` + +## Authentication Options + +The `AzureKeyVaultSigner` uses Azure Identity's `DefaultAzureCredential` by default, which +tries multiple authentication methods in order: + +1. **Environment variables** - `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET` +2. **Managed Identity** - When running on Azure (App Service, Functions, VMs, etc.) +3. **Azure CLI** - When authenticated via `az login` +4. **Azure PowerShell** - When authenticated via PowerShell +5. **Visual Studio Code** - When signed in to Azure in VS Code + +You can also provide a specific credential: + +```typescript +import { ManagedIdentityCredential, EnvironmentCredential } from '@azure/identity'; + +// Use Managed Identity +const credential = new ManagedIdentityCredential(); + +// Or use environment variables +const credential = new EnvironmentCredential(); + +const signer = new AzureKeyVaultSigner({ + keyVaultUrl: 'https://my-vault.vault.azure.net', + keyName: 'my-signing-key', + credential, +}); +``` + +## Error Handling + +```typescript +try { + const signedDoc = await doc.sign({ + signer, + issuerCertificate: certificatePEM, + alg: 'ES256', + }); +} catch (error) { + if (error.message.includes('Azure Key Vault')) { + console.error('Azure Key Vault error:', error); + // Handle Azure-specific errors (permissions, network, etc.) + } else if (error.message.includes('algorithm')) { + console.error('Algorithm mismatch:', error); + // Key algorithm doesn't match the requested algorithm + } else { + console.error('Signing error:', error); + } +} +``` + +## Benefits of Azure Key Vault Signing + +1. **Security** - Private keys never leave Azure Key Vault's HSM +2. **Compliance** - Meets regulatory requirements for key management (FIPS 140-2, etc.) +3. **Audit Trail** - All signing operations are logged in Azure Monitor +4. **Key Rotation** - Easily rotate keys without changing application code (use key versions) +5. **Access Control** - Fine-grained access control using Azure RBAC and Key Vault policies + +## Complete Example + +```typescript +import { Document, AzureKeyVaultSigner } from '@auth0/mdl'; +import * as fs from 'fs'; + +async function signDocument() { + // Read your issuer certificate + const certificatePEM = fs.readFileSync('./issuer-cert.pem', 'utf-8'); + + // Create Azure Key Vault signer + const signer = new AzureKeyVaultSigner({ + keyVaultUrl: process.env.AZURE_KEYVAULT_URL!, + keyName: process.env.AZURE_KEY_NAME!, + }); + + // Create document + const doc = new Document('org.iso.18013.5.1.mDL'); + + // Add driver's license data + doc.addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Smith', + given_name: 'Alice', + birth_date: '1985-05-15', + issue_date: '2024-01-15', + expiry_date: '2034-01-15', + issuing_country: 'US', + issuing_authority: 'CA DMV', + document_number: 'D1234567', + portrait: portraitImageBytes, // Your portrait image as Uint8Array + driving_privileges: [ + { + vehicle_category_code: 'C', + issue_date: '2024-01-15', + expiry_date: '2034-01-15', + }, + ], + }); + + // Add device key + doc.addDeviceKeyInfo({ deviceKey: devicePublicKeyJWK }); + + // Set validity + doc.addValidityInfo({ + signed: new Date(), + validFrom: new Date(), + validUntil: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year + }); + + // Sign with Azure Key Vault + const signedDoc = await doc.sign({ + signer, + issuerCertificate: certificatePEM, + alg: 'ES256', + }); + + // Encode to CBOR + const encodedDoc = signedDoc.encode(); + + console.log('Document signed and encoded successfully!'); + return encodedDoc; +} + +signDocument().catch(console.error); +``` + +## Local Development vs Production + +For local development, you can use Azure CLI authentication: + +```bash +az login +az account set --subscription +``` + +For production, use Managed Identity or service principals with appropriate Key Vault access +policies. + +## See Also + +- [Azure Key Vault Documentation](https://docs.microsoft.com/en-us/azure/key-vault/) +- [Azure Identity SDK](https://docs.microsoft.com/en-us/javascript/api/overview/azure/ + identity-readme) +- [ISO 18013-5 mDL Standard](https://www.iso.org/standard/69084.html) diff --git a/AZURE_KEY_VAULT_INTEGRATION.md b/AZURE_KEY_VAULT_INTEGRATION.md new file mode 100644 index 0000000..3055b08 --- /dev/null +++ b/AZURE_KEY_VAULT_INTEGRATION.md @@ -0,0 +1,204 @@ +# Azure Key Vault Integration for @auth0/mdl + +This document describes the Azure Key Vault signing integration added to the `@auth0/mdl` library. + +## Overview + +This feature adds support for signing mDL/MDOC documents using private keys stored in Azure +Key Vault, without exposing the private key material to the application. This provides enhanced +security for production environments and meets compliance requirements for key management. + +## Architecture + +The implementation uses a **Signer abstraction layer** that allows for multiple signing backends: + +``` +Document.sign() + ├─> issuerPrivateKey (traditional, backward compatible) + └─> signer: Signer interface + ├─> LocalKeySigner (wraps local JWK keys) + └─> AzureKeyVaultSigner (signs via Azure Key Vault) +``` + +### Key Components + +1. **Signer Interface** (`src/mdoc/signing/Signer.ts`) + - Abstract interface for signing operations + - Supports `sign()`, `getPublicKey()`, and `getKeyId()` methods + +2. **AzureKeyVaultSigner** (`src/mdoc/signing/AzureKeyVaultSigner.ts`) + - Implements Signer interface using Azure Key Vault + - Uses `@azure/keyvault-keys` and `@azure/identity` SDKs + - Automatically hashes data and maps COSE algorithms to Azure signature algorithms + +3. **LocalKeySigner** (`src/mdoc/signing/LocalKeySigner.ts`) + - Implements Signer interface for local JWK keys + - Provides backward compatibility and testing convenience + +4. **IssuerAuth.signWithSigner()** (`src/mdoc/model/IssuerAuth.ts`) + - New static method that constructs COSE_Sign1 structure manually + - Bypasses cose-kit's key handling to support custom signers + - Follows RFC 8152 Section 4.4 for Sig_structure construction + +5. **Document.sign() Updates** (`src/mdoc/model/Document.ts`) + - Extended to accept optional `signer` parameter + - Validates that either `issuerPrivateKey` OR `signer` is provided (mutually exclusive) + - Routes to appropriate signing method based on input + +## Changes Made + +### New Files + +- `src/mdoc/signing/Signer.ts` - Signer interface +- `src/mdoc/signing/LocalKeySigner.ts` - Local key implementation +- `src/mdoc/signing/AzureKeyVaultSigner.ts` - Azure Key Vault implementation +- `src/mdoc/signing/index.ts` - Module exports +- `AZURE_KEY_VAULT_EXAMPLE.md` - Usage examples and documentation + +### Modified Files + +- `src/mdoc/model/IssuerAuth.ts` + - Added `signWithSigner()` static method + - Added COSE header/algorithm constants and helper functions + +- `src/mdoc/model/Document.ts` + - Modified `sign()` to accept optional `signer` parameter + - Added validation logic for mutually exclusive parameters + - Integrated custom signer path + +- `src/index.ts` + - Exported new signing classes and interfaces + +- `package.json` + - Added `@azure/identity` (^4.0.0) + - Added `@azure/keyvault-keys` (^4.8.0) + +## Usage + +### Quick Start + +```typescript +import { Document, AzureKeyVaultSigner } from '@auth0/mdl'; + +const signer = new AzureKeyVaultSigner({ + keyVaultUrl: 'https://my-vault.vault.azure.net', + keyName: 'my-signing-key', +}); + +const doc = new Document('org.iso.18013.5.1.mDL'); +// ... add namespaces ... + +const signedDoc = await doc.sign({ + signer, + issuerCertificate: certificatePEM, + alg: 'ES256', +}); +``` + +### Backward Compatibility + +Existing code continues to work without changes: + +```typescript +const signedDoc = await doc.sign({ + issuerPrivateKey: jwkPrivateKey, // Still works! + issuerCertificate: certificatePEM, + alg: 'ES256', +}); +``` + +## Security Benefits + +1. **Private Key Protection** - Keys never leave Azure Key Vault's HSM +2. **Compliance** - Meets FIPS 140-2 and other regulatory requirements +3. **Audit Logging** - All signing operations logged in Azure Monitor +4. **Access Control** - Fine-grained permissions via Azure RBAC +5. **Key Rotation** - Support for key versioning and rotation + +## Supported Algorithms + +Azure Key Vault HSM supports the following algorithms: + +- **ES256** - ECDSA using P-256 and SHA-256 ✅ +- **ES384** - ECDSA using P-384 and SHA-384 ✅ +- **ES512** - ECDSA using P-521 and SHA-512 ✅ + +**Not Supported:** +- **EdDSA** (Ed25519) ❌ - Azure Key Vault HSM does not support Ed25519 keys. Use + `LocalKeySigner` for EdDSA. + +## Technical Details + +### COSE_Sign1 Structure + +The `IssuerAuth.signWithSigner()` method manually constructs the COSE_Sign1 Sig_structure: + +``` +Sig_structure = [ + context: "Signature1", + protected: serialized_protected_headers, + external_aad: empty_uint8array, + payload: mso_payload +] +``` + +This is CBOR-encoded and passed to the signer's `sign()` method. + +### Azure Key Vault Integration + +The AzureKeyVaultSigner: +1. Hashes the data using the appropriate algorithm (SHA-256, SHA-384, or SHA-512) +2. Maps COSE algorithm names to Azure signature algorithms +3. Calls `CryptographyClient.sign()` with the digest +4. Returns the signature as a `Uint8Array` + +### Type Compatibility + +The implementation works with the installed version of `cose-kit` (v1.7.1), which uses plain +object types for `ProtectedHeaders` and `UnprotectedHeaders` rather than classes. Helper +functions (`protectedHeadersToMap`, `unprotectedHeadersToMap`) convert these to CBOR-encodable +Maps. + +## Testing + +To test the Azure Key Vault integration: + +1. Set up an Azure Key Vault with an EC key +2. Configure Azure credentials (CLI, environment variables, or managed identity) +3. Run the example: + +```typescript +// See AZURE_KEY_VAULT_EXAMPLE.md for complete example +``` + +## Future Enhancements + +Potential future additions: +- AWS KMS support (`AwsKmsSigner`) +- Google Cloud KMS support (`GcpKmsSigner`) +- Hardware Security Module (HSM) support +- Key rotation strategies +- Performance optimizations (key caching) + +## Contributing + +When contributing additional Signer implementations: + +1. Implement the `Signer` interface +2. Handle algorithm mapping appropriately +3. Ensure proper error handling +4. Add comprehensive tests +5. Document usage examples + +## References + +- [RFC 8152 - COSE (CBOR Object Signing and Encryption)] + (https://datatracker.ietf.org/doc/html/rfc8152) +- [ISO 18013-5 - mDL Standard](https://www.iso.org/standard/69084.html) +- [Azure Key Vault Documentation](https://docs.microsoft.com/en-us/azure/key-vault/) +- [Azure Identity SDK](https://docs.microsoft.com/en-us/javascript/api/overview/azure/ + identity-readme) + +## License + +This feature follows the same license as the @auth0/mdl library (Apache-2.0). diff --git a/__tests__/issuing/azureKeyVault.tests.ts b/__tests__/issuing/azureKeyVault.tests.ts new file mode 100644 index 0000000..39bc228 --- /dev/null +++ b/__tests__/issuing/azureKeyVault.tests.ts @@ -0,0 +1,429 @@ +import * as jose from 'jose'; +import { COSEKeyToJWK } from 'cose-kit'; +import { ClientSecretCredential } from '@azure/identity'; +import { + MDoc, + Document, + Verifier, + parse, + IssuerSignedDocument, + AzureKeyVaultSigner, +} from '../../src'; +import { DEVICE_JWK, ISSUER_CERTIFICATE } from './config'; + +const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; + +// Azure Key Vault certificate chain (leaf + intermediate + root) +// Full chain for embedding in the x5chain header +const AZURE_KEY_VAULT_CERTIFICATE_CHAIN = `-----BEGIN CERTIFICATE----- +MIID1jCCA3ygAwIBAgIUddQy2FOR8KrfEaXGToyfRw+6VYkwCgYIKoZIzj0EAwIw +gYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T +YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT +ZWN1cml0eTEiMCAGA1UEAwwZTXlDb21wYW55IEludGVybWVkaWF0ZSBDQTAeFw0y +NTEwMjcxNDIyNDJaFw0yNjEwMjcxNDIyNDJaMHAxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRYwFAYDVQQK +DA1NeUNvbXBhbnkgSW5jMRwwGgYDVQQDDBNsZWFmMS5teWNvbXBhbnkuY29tMFkw +EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQWHhjGbA4ZZEZasF62pYctQ8tHX0HMDJ +Jb0QgiTaq76b+FMZWuQ/SU2wIV9K4JfjVbLWU4JuyQO+xr5tKoA+vqOCAdgwggHU +MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUF +BwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUatgdVmySrerOBDjCpM4Og9A6hHowHwYD +VR0jBBgwFoAUhYS70VQN/x4zszXTuFXnmlwuD6YwcgYDVR0RBGswaYITbGVhZjEu +bXljb21wYW55LmNvbYIXd3d3LmxlYWYxLm15Y29tcGFueS5jb22CFSoubGVhZjEu +bXljb21wYW55LmNvbYcEwKgBZIYcaHR0cHM6Ly9sZWFmMS5teWNvbXBhbnkuY29t +LzB6BgNVHR8EczBxMDOgMaAvhi1odHRwOi8vY3JsLm15Y29tcGFueS5jb20vbXlj +b21wYW55LWludC1jYS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwtYmFja3VwLm15Y29t +cGFueS5jb20vbXljb21wYW55LWludC1jYS5jcmwwZQYIKwYBBQUHAQEEWTBXMCUG +CCsGAQUFBzABhhlodHRwOi8vb2NzcC5teWNvbXBhbnkuY29tMC4GCCsGAQUFBzAC +hiJodHRwOi8vY2EubXljb21wYW55LmNvbS9pbnQtY2EuY3J0MAoGCCqGSM49BAMC +A0gAMEUCIBACN4AUAB8y360eWaX5i6dZ7VL+MOQNm/YSUcozrWYTAiEArvMCGoep +W8VOY3Z3NNLlvdJFX8WzgV4RjBClXUcNjQA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID1jCCA3ugAwIBAgIUM51uNu3ULLYO71dDQK25v/Qr/sEwCgYIKoZIzj0EAwIw +gYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T +YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT +ZWN1cml0eTEaMBgGA1UEAwwRTXlDb21wYW55IFJvb3QgQ0EwHhcNMjUxMDI3MTQy +MjQwWhcNMzAxMDI2MTQyMjQwWjCBiTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh +bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoMDU15Q29t +cGFueSBJbmMxETAPBgNVBAsMCFNlY3VyaXR5MSIwIAYDVQQDDBlNeUNvbXBhbnkg +SW50ZXJtZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKZ7u2UVX +i34ILZhRc6Y2xgcLLV7V7uaIdry3zWIGerJRYBl9qc5QE+c6vhUoLr3uXJ8Ypsc5 +axT88I+s40HcgaOCAcUwggHBMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBSFhLvRVA3/HjOzNdO4VeeaXC4PpjAfBgNVHSMEGDAW +gBQIYDTeNr2PmcPzVVaVPsyAZCSRijB4BgNVHREEcTBvghRpbnQtY2EubXljb21w +YW55LmNvbYIdaW50ZXJtZWRpYXRlLWNhLm15Y29tcGFueS5jb22GHGh0dHA6Ly9p +bnQtY2EubXljb21wYW55LmNvbS+BGmludC1jYS1hZG1pbkBteWNvbXBhbnkuY29t +MHoGA1UdHwRzMHEwM6AxoC+GLWh0dHA6Ly9jcmwubXljb21wYW55LmNvbS9teWNv +bXBhbnktaW50LWNhLmNybDA6oDigNoY0aHR0cDovL2NybC1iYWNrdXAubXljb21w +YW55LmNvbS9teWNvbXBhbnktaW50LWNhLmNybDBlBggrBgEFBQcBAQRZMFcwJQYI +KwYBBQUHMAGGGWh0dHA6Ly9vY3NwLm15Y29tcGFueS5jb20wLgYIKwYBBQUHMAKG +Imh0dHA6Ly9jYS5teWNvbXBhbnkuY29tL2ludC1jYS5jcnQwCgYIKoZIzj0EAwID +SQAwRgIhAJl5fDVmA9zRVvPIXVDLeHD0YL8uoU13EEFKN/GcvC52AiEA2jdtwrNR +tgWbVSREsxsK/9ybfUyaUhCcg1jlrwutg7E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDvDCCA2KgAwIBAgIUe/TFUO8Fc+Ws6vLcZSOBIf09PbQwCgYIKoZIzj0EAwIw +gYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T +YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT +ZWN1cml0eTEaMBgGA1UEAwwRTXlDb21wYW55IFJvb3QgQ0EwHhcNMjUxMDI3MTQy +MjM5WhcNMzUxMDI1MTQyMjM5WjCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh +bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoMDU15Q29t +cGFueSBJbmMxETAPBgNVBAsMCFNlY3VyaXR5MRowGAYDVQQDDBFNeUNvbXBhbnkg +Um9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGbH9E6FoEc7JulkOTFW +/opse0bQtM+rVBxiZkrE4RQ1XYgIfT/SDERX/HbRHsvfTqzPc9i2ICnP1yJsfx0W +FiSjggG0MIIBsDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUCGA03ja9j5nD81VWlT7MgGQkkYowHwYDVR0jBBgwFoAUCGA03ja9 +j5nD81VWlT7MgGQkkYowZAYDVR0RBF0wW4IQY2EubXljb21wYW55LmNvbYIVcm9v +dC1jYS5teWNvbXBhbnkuY29thhhodHRwOi8vY2EubXljb21wYW55LmNvbS+BFmNh +LWFkbWluQG15Y29tcGFueS5jb20wfAYDVR0fBHUwczA0oDKgMIYuaHR0cDovL2Ny +bC5teWNvbXBhbnkuY29tL215Y29tcGFueS1yb290LWNhLmNybDA7oDmgN4Y1aHR0 +cDovL2NybC1iYWNrdXAubXljb21wYW55LmNvbS9teWNvbXBhbnktcm9vdC1jYS5j +cmwwZgYIKwYBBQUHAQEEWjBYMCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5teWNv +bXBhbnkuY29tMC8GCCsGAQUFBzAChiNodHRwOi8vY2EubXljb21wYW55LmNvbS9y +b290LWNhLmNydDAKBggqhkjOPQQDAgNIADBFAiEA/U11iSXRvzPfriPTkJIX+zgc ++55+glglVUTF81akovUCIFvoXh/jh+fanzVNEIOeLMdUzP7ZeSIwlbjpbSVOGjWM +-----END CERTIFICATE-----`; + +// Root CA certificate for verification +const AZURE_KEY_VAULT_ROOT_CERTIFICATE = `-----BEGIN CERTIFICATE----- +MIIDvDCCA2KgAwIBAgIUe/TFUO8Fc+Ws6vLcZSOBIf09PbQwCgYIKoZIzj0EAwIw +gYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T +YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT +ZWN1cml0eTEaMBgGA1UEAwwRTXlDb21wYW55IFJvb3QgQ0EwHhcNMjUxMDI3MTQy +MjM5WhcNMzUxMDI1MTQyMjM5WjCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh +bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoMDU15Q29t +cGFueSBJbmMxETAPBgNVBAsMCFNlY3VyaXR5MRowGAYDVQQDDBFNeUNvbXBhbnkg +Um9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGbH9E6FoEc7JulkOTFW +/opse0bQtM+rVBxiZkrE4RQ1XYgIfT/SDERX/HbRHsvfTqzPc9i2ICnP1yJsfx0W +FiSjggG0MIIBsDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUCGA03ja9j5nD81VWlT7MgGQkkYowHwYDVR0jBBgwFoAUCGA03ja9 +j5nD81VWlT7MgGQkkYowZAYDVR0RBF0wW4IQY2EubXljb21wYW55LmNvbYIVcm9v +dC1jYS5teWNvbXBhbnkuY29thhhodHRwOi8vY2EubXljb21wYW55LmNvbS+BFmNh +LWFkbWluQG15Y29tcGFueS5jb20wfAYDVR0fBHUwczA0oDKgMIYuaHR0cDovL2Ny +bC5teWNvbXBhbnkuY29tL215Y29tcGFueS1yb290LWNhLmNybDA7oDmgN4Y1aHR0 +cDovL2NybC1iYWNrdXAubXljb21wYW55LmNvbS9teWNvbXBhbnktcm9vdC1jYS5j +cmwwZgYIKwYBBQUHAQEEWjBYMCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5teWNv +bXBhbnkuY29tMC8GCCsGAQUFBzAChiNodHRwOi8vY2EubXljb21wYW55LmNvbS9y +b290LWNhLmNydDAKBggqhkjOPQQDAgNIADBFAiEA/U11iSXRvzPfriPTkJIX+zgc ++55+glglVUTF81akovUCIFvoXh/jh+fanzVNEIOeLMdUzP7ZeSIwlbjpbSVOGjWM +-----END CERTIFICATE-----`; + +// Check if Azure environment variables are set +const isAzureConfigured = () => { + return !!( + process.env.AZURE_KEYVAULT_URL && + process.env.AZURE_TENANT_ID && + process.env.AZURE_CLIENT_ID && + process.env.AZURE_CLIENT_SECRET + ); +}; + +// Helper to get Azure configuration +const getAzureConfig = () => { + if (!isAzureConfigured()) { + return null; + } + + return { + keyVaultUrl: process.env.AZURE_KEYVAULT_URL!, + tenantId: process.env.AZURE_TENANT_ID!, + clientId: process.env.AZURE_CLIENT_ID!, + clientSecret: process.env.AZURE_CLIENT_SECRET!, + keyName: 'leaf-1-ec-hsm-key', // The HSM-managed ES256 key + }; +}; + +// Conditional test suite +const describeIfAzureConfigured = isAzureConfigured() ? describe : describe.skip; + +describeIfAzureConfigured('Azure Key Vault integration', () => { + let azureConfig: ReturnType; + let signer: AzureKeyVaultSigner; + + // Get config inside beforeAll to avoid evaluation when tests are skipped + beforeAll(() => { + azureConfig = getAzureConfig(); + if (!azureConfig) { + throw new Error('Azure configuration not available'); + } + }); + + beforeAll(() => { + if (!azureConfig) return; + + // Create Azure credential + const credential = new ClientSecretCredential( + azureConfig.tenantId, + azureConfig.clientId, + azureConfig.clientSecret, + ); + + // Create the signer + signer = new AzureKeyVaultSigner({ + keyVaultUrl: azureConfig.keyVaultUrl, + keyName: azureConfig.keyName, + credential, + }); + }); + + describe('AzureKeyVaultSigner', () => { + it('should initialize successfully', () => { + expect(signer).toBeDefined(); + expect(signer.getKeyId()).toBe(azureConfig!.keyName); + }); + + it('should retrieve the public key from Azure Key Vault', async () => { + const publicKey = await signer.getPublicKey(); + expect(publicKey).toBeDefined(); + expect(publicKey.kty).toBe('EC'); + expect(publicKey.crv).toBe('P-256'); // ES256 uses P-256 + expect(publicKey.x).toBeDefined(); + expect(publicKey.y).toBeDefined(); + expect(publicKey.d).toBeUndefined(); // Should not have private key component + }, 10000); // 10 second timeout for network call + + it('should sign data using Azure Key Vault', async () => { + const testData = new Uint8Array([1, 2, 3, 4, 5]); + const signature = await signer.sign('ES256', testData); + + expect(signature).toBeDefined(); + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBeGreaterThan(0); + // ES256 signatures are typically 64 bytes (DER encoded can be slightly longer) + expect(signature.length).toBeGreaterThanOrEqual(64); + }, 10000); + }); + + describe('issuing an MDOC with Azure Key Vault', () => { + let encoded: Uint8Array; + let parsedDocument: IssuerSignedDocument; + + // Use dates within the certificate validity period (Oct 27, 2025 - Oct 27, 2026) + // Truncate to seconds to match CBOR encoding + const now = new Date(); + now.setMilliseconds(0); + const signed = new Date(now); + signed.setHours(signed.getHours() - 1); // Signed 1 hour ago + const validFrom = new Date(signed); + validFrom.setMinutes(signed.getMinutes() - 10); // Valid from 10 minutes before signing + const validUntil = new Date(signed); + validUntil.setMonth(signed.getMonth() + 6); // 6 months validity + const expectedUpdate = new Date(signed); + expectedUpdate.setMonth(signed.getMonth() + 1); // Update in 1 month + + beforeAll(async () => { + const document = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Smith', + given_name: 'Alice', + birth_date: '1990-05-15', + issue_date: '2024-01-15', + expiry_date: '2034-01-15', + issuing_country: 'US', + issuing_authority: 'CA DMV', + document_number: 'AKV123456', + portrait: 'bstr', + driving_privileges: [ + { + vehicle_category_code: 'C', + issue_date: '2024-01-15', + expiry_date: '2034-01-15', + }, + ], + }) + .useDigestAlgorithm('SHA-256') + .addValidityInfo({ + signed, + validFrom, + validUntil, + expectedUpdate, + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + signer, // Use Azure Key Vault signer + issuerCertificate: AZURE_KEY_VAULT_CERTIFICATE_CHAIN, + alg: 'ES256', + }); + + const mdoc = new MDoc([document]); + const encodedBuffer = mdoc.encode(); + encoded = new Uint8Array(encodedBuffer); + + const parsedMDOC = parse(encoded); + [parsedDocument] = parsedMDOC.documents; + }, 15000); // 15 second timeout for signing operation + + it('should create a valid signed document', () => { + expect(encoded).toBeDefined(); + expect(encoded).toBeInstanceOf(Uint8Array); + expect(encoded.length).toBeGreaterThan(0); + }); + + it('should be verifiable with the matching Azure Key Vault certificate', async () => { + // Full cryptographic signature verification with matching certificate + const verifier = new Verifier([AZURE_KEY_VAULT_ROOT_CERTIFICATE]); + await verifier.verify(encoded, { + onCheck: (verification, original) => { + // Skip device auth verification since we're only testing issuer signature + if (verification.category === 'DEVICE_AUTH') { + return; + } + original(verification); + }, + }); + + // Verify structure as well + expect(parsedDocument).toBeDefined(); + expect(parsedDocument.issuerSigned).toBeDefined(); + expect(parsedDocument.issuerSigned.issuerAuth).toBeDefined(); + expect(parsedDocument.issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); + expect(parsedDocument.issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); + }); + + it('should contain the correct validity info', () => { + const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(validityInfo).toBeDefined(); + // Dates should match exactly since we truncated milliseconds + expect(validityInfo.signed).toEqual(signed); + expect(validityInfo.validFrom).toEqual(validFrom); + expect(validityInfo.validUntil).toEqual(validUntil); + expect(validityInfo.expectedUpdate).toBeDefined(); + expect(validityInfo.expectedUpdate).toEqual(expectedUpdate); + }); + + it('should use the correct digest algorithm', () => { + const { digestAlgorithm } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(digestAlgorithm).toEqual('SHA-256'); + }); + + it('should include the device public key', () => { + const { deviceKeyInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(deviceKeyInfo?.deviceKey).toBeDefined(); + const actual = typeof deviceKeyInfo !== 'undefined' && + COSEKeyToJWK(deviceKeyInfo.deviceKey); + expect(actual).toEqual(publicKeyJWK); + }); + + it('should include the namespace and attributes', () => { + const attrValues = parsedDocument.getIssuerNameSpace('org.iso.18013.5.1'); + expect(attrValues).toBeDefined(); + expect(attrValues.family_name).toBe('Smith'); + expect(attrValues.given_name).toBe('Alice'); + expect(attrValues.birth_date.toString()).toBe('1990-05-15'); + expect(attrValues.document_number).toBe('AKV123456'); + expect(attrValues.issuing_country).toBe('US'); + expect(attrValues.issuing_authority).toBe('CA DMV'); + }); + + it('should have a valid COSE signature structure', () => { + const { issuerAuth } = parsedDocument.issuerSigned; + expect(issuerAuth).toBeDefined(); + expect(issuerAuth.payload).toBeDefined(); + expect(issuerAuth.signature).toBeDefined(); + expect(issuerAuth.signature.length).toBeGreaterThan(0); + }); + }); + + describe('comparison with local key signing', () => { + it('both should produce valid COSE structures', async () => { + // Sign with Azure Key Vault + const azureDoc = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Test', + given_name: 'Azure', + birth_date: '1990-01-01', + issue_date: '2024-01-01', + expiry_date: '2034-01-01', + issuing_country: 'US', + document_number: 'AZ001', + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + signer, + issuerCertificate: AZURE_KEY_VAULT_CERTIFICATE_CHAIN, + alg: 'ES256', + }); + + // Sign with local key (traditional method) + const localDoc = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Test', + given_name: 'Local', + birth_date: '1990-01-01', + issue_date: '2024-01-01', + expiry_date: '2034-01-01', + issuing_country: 'US', + document_number: 'LOC001', + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + issuerPrivateKey: { + kty: 'EC', + kid: '1234', + x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', + y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', + crv: 'P-256', + d: 'o6PrzBm1dCfSwqJHW6DVqmJOCQSIAosrCPfbFJDMNp4', + }, + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + }); + + // Both should encode successfully + const azureEncoded = new Uint8Array(new MDoc([azureDoc]).encode()); + const localEncoded = new Uint8Array(new MDoc([localDoc]).encode()); + + expect(azureEncoded).toBeDefined(); + expect(localEncoded).toBeDefined(); + + // Both should be parseable + const parsedAzure = parse(azureEncoded); + const parsedLocal = parse(localEncoded); + + expect(parsedAzure.documents).toHaveLength(1); + expect(parsedLocal.documents).toHaveLength(1); + + // Local doc should be verifiable with its matching certificate + const localVerifier = new Verifier([ISSUER_CERTIFICATE]); + await localVerifier.verify(localEncoded, { + onCheck: (v, o) => (v.category === 'DEVICE_AUTH' ? undefined : o(v)), + }); + + // Azure doc should also be verifiable with its matching certificate + const azureVerifier = new Verifier([AZURE_KEY_VAULT_ROOT_CERTIFICATE]); + await azureVerifier.verify(azureEncoded, { + onCheck: (v, o) => (v.category === 'DEVICE_AUTH' ? undefined : o(v)), + }); + + // Both signatures should be valid + expect(parsedAzure.documents[0].issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); + expect(parsedAzure.documents[0].issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); + expect(parsedLocal.documents[0].issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); + expect(parsedLocal.documents[0].issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); + }, 15000); + }); +}); + +// Always run this describe block to show helpful message when Azure is not configured +describe('Azure Key Vault integration (configuration check)', () => { + if (!isAzureConfigured()) { + it('should skip tests when Azure environment is not configured', () => { + console.log('\n⚠️ Azure Key Vault tests skipped'); + console.log('To enable these tests, source the azure-env.sh file:'); + console.log(' source azure-env.sh'); + console.log(' npm test'); + console.log('\nRequired environment variables:'); + console.log(' - AZURE_KEYVAULT_URL'); + console.log(' - AZURE_TENANT_ID'); + console.log(' - AZURE_CLIENT_ID'); + console.log(' - AZURE_CLIENT_SECRET\n'); + }); + } else { + it('Azure environment is configured', () => { + const config = getAzureConfig(); + expect(config).toBeDefined(); + expect(config?.keyVaultUrl).toBeDefined(); + expect(config?.keyName).toBe('leaf-1-ec-hsm-key'); + }); + } +}); diff --git a/package-lock.json b/package-lock.json index 0518aad..a7a81f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.5.0", "license": "Apache-2.0", "dependencies": { + "@azure/identity": "^4.0.0", + "@azure/keyvault-keys": "^4.8.0", "@noble/curves": "^1.2.0", "@panva/hkdf": "^1.1.1", "@peculiar/x509": "^1.9.5", @@ -66,6 +68,264 @@ "node": ">=6.0.0" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.1.tgz", + "integrity": "sha512-UVZlVLfLyz6g3Hy7GNDpooMQonUygH7ghdiSASOOHy97fKj/mPLqgDX7aidOijn+sCMU+WU8NjlPlNTgnvbcGA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.25.1.tgz", + "integrity": "sha512-kAdOSNjvMbeBmEyd5WnddGmIpKCbAAGj4Gg/1iURtF+nHmIfS0+QUBBO3uaHl7CBB2R1SEAbpOgxycEwrHOkFA==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.13.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", + "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.0.tgz", + "integrity": "sha512-23BXm82Mp5XnRhrcd4mrHa0xuUNRp96ivu3nRatrfdAqjoeWAGyD0eEAafxAOHAEWWmdlyFK4ELFcdziXyw2sA==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.13.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2583,6 +2843,20 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz", + "integrity": "sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -2807,6 +3081,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3297,6 +3580,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3309,6 +3598,21 @@ "integrity": "sha512-9vkiAYT+3X1wE9A3kfrUV+P2qO7O6jefYTkvylkP7+/UUM+tTYxt3fT2BVCXC+0EFJi1P/Z4NRCYgjFcxiAKkQ==", "dev": true }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytestreamjs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", @@ -3712,6 +4016,46 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -3792,6 +4136,15 @@ "node": ">=8" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.504", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.504.tgz", @@ -4958,6 +5311,32 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5198,6 +5577,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5237,6 +5631,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -5400,6 +5812,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -6143,6 +6570,49 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -6220,11 +6690,40 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -6250,6 +6749,12 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -6593,6 +7098,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -7074,6 +7597,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7119,7 +7654,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -7134,8 +7668,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.0.0", @@ -7175,7 +7708,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -7717,9 +8249,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -7940,6 +8473,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -8164,6 +8706,21 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8236,6 +8793,196 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "requires": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + } + }, + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "requires": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + } + }, + "@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "requires": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + } + }, + "@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "requires": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + } + }, + "@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@azure/core-rest-pipeline": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.1.tgz", + "integrity": "sha512-UVZlVLfLyz6g3Hy7GNDpooMQonUygH7ghdiSASOOHy97fKj/mPLqgDX7aidOijn+sCMU+WU8NjlPlNTgnvbcGA==", + "requires": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + } + }, + "@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "requires": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + } + }, + "@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + } + }, + "@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + } + }, + "@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "requires": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + } + }, + "@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "requires": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + } + }, + "@azure/msal-browser": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.25.1.tgz", + "integrity": "sha512-kAdOSNjvMbeBmEyd5WnddGmIpKCbAAGj4Gg/1iURtF+nHmIfS0+QUBBO3uaHl7CBB2R1SEAbpOgxycEwrHOkFA==", + "requires": { + "@azure/msal-common": "15.13.0" + } + }, + "@azure/msal-common": { + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", + "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==" + }, + "@azure/msal-node": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.0.tgz", + "integrity": "sha512-23BXm82Mp5XnRhrcd4mrHa0xuUNRp96ivu3nRatrfdAqjoeWAGyD0eEAafxAOHAEWWmdlyFK4ELFcdziXyw2sA==", + "requires": { + "@azure/msal-common": "15.13.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + } + }, "@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -10071,6 +10818,16 @@ "eslint-visitor-keys": "^3.4.1" } }, + "@typespec/ts-http-runtime": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz", + "integrity": "sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==", + "requires": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + } + }, "@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -10267,6 +11024,11 @@ "dev": true, "requires": {} }, + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10603,6 +11365,11 @@ "ieee754": "^1.2.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -10615,6 +11382,14 @@ "integrity": "sha512-9vkiAYT+3X1wE9A3kfrUV+P2qO7O6jefYTkvylkP7+/UUM+tTYxt3fT2BVCXC+0EFJi1P/Z4NRCYgjFcxiAKkQ==", "dev": true }, + "bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "requires": { + "run-applescript": "^7.0.0" + } + }, "bytestreamjs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", @@ -10890,6 +11665,25 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, + "default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "requires": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + } + }, + "default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==" + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==" + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -10945,6 +11739,14 @@ "is-obj": "^2.0.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "electron-to-chromium": { "version": "1.4.504", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.504.tgz", @@ -11796,6 +12598,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -11949,6 +12769,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -11976,6 +12801,14 @@ "is-extglob": "^2.1.1" } }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "requires": { + "is-docker": "^3.0.0" + } + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -12079,6 +12912,14 @@ "call-bind": "^1.0.2" } }, + "is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "requires": { + "is-inside-container": "^1.0.0" + } + }, "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -12647,6 +13488,42 @@ "through": ">=2.2.7 <3" } }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, + "jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -12706,11 +13583,35 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "lodash.kebabcase": { "version": "4.1.1", @@ -12736,6 +13637,11 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -13000,6 +13906,17 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "requires": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + } + }, "optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -13332,6 +14249,11 @@ "glob": "^7.1.3" } }, + "run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==" + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13356,9 +14278,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "peer": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex-test": { "version": "1.0.0", @@ -13386,8 +14306,7 @@ "semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "serialize-javascript": { "version": "6.0.2", @@ -13763,9 +14682,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "tsutils": { "version": "3.21.0", @@ -13915,6 +14834,11 @@ "punycode": "^2.1.0" } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -14087,6 +15011,14 @@ "signal-exit": "^3.0.7" } }, + "wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "requires": { + "is-wsl": "^3.1.0" + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index f402c26..9f5f7ba 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "typescript": "^5.1.6" }, "dependencies": { + "@azure/identity": "^4.0.0", + "@azure/keyvault-keys": "^4.8.0", "@noble/curves": "^1.2.0", "@panva/hkdf": "^1.1.1", "@peculiar/x509": "^1.9.5", diff --git a/sample-azure-env.sh b/sample-azure-env.sh new file mode 100755 index 0000000..6ed33cd --- /dev/null +++ b/sample-azure-env.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# +# Usage: source azure-env.sh + +# Azure KeyVault URL +# Format: https://.vault.azure.net +export AZURE_KEYVAULT_URL="" + +# Azure Active Directory Tenant ID +# Find this in Azure Portal -> Azure Active Directory -> Properties -> Tenant ID +export AZURE_TENANT_ID="" + +# Service Principal Client ID (Application ID) +# Find this in Azure Portal -> Azure Active Directory -> App registrations -> +# Your App -> Application ID +export AZURE_CLIENT_ID="" + +# Service Principal Client Secret +# Generate this in Azure Portal -> Azure Active Directory -> App registrations -> +# Your App -> Certificates & secrets +export AZURE_CLIENT_SECRET="" + + +echo "Azure KeyVault environment variables set:" +echo " AZURE_KEYVAULT_URL: $AZURE_KEYVAULT_URL" +echo " AZURE_TENANT_ID: $AZURE_TENANT_ID" +echo " AZURE_CLIENT_ID: $AZURE_CLIENT_ID" +echo " AZURE_CLIENT_SECRET: ****" + +# Instructions for setting up Azure Service Principal +cat <<'EOF' + +To create a Service Principal with Key Vault access: + +1. Create a service principal: + az ad sp create-for-rbac --name "OpenSSL-KeyVault-Provider" \ + --skip-assignment + +2. Grant Key Vault permissions: + az keyvault set-policy --name \ + --spn \ + --key-permissions get list sign verify encrypt decrypt wrapKey unwrapKey \ + --secret-permissions get list \ + --certificate-permissions get list + +3. Copy the output values to this file: + - appId -> AZURE_CLIENT_ID + - password -> AZURE_CLIENT_SECRET + - tenant -> AZURE_TENANT_ID + +4. Set your vault URL: + - AZURE_KEYVAULT_URL (format: https://.vault.azure.net) + +For more information: +https://docs.microsoft.com/en-us/azure/key-vault/general/authentication + +EOF diff --git a/src/index.ts b/src/index.ts index 25ef185..81b3060 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,4 @@ export { DeviceResponse } from './mdoc/model/DeviceResponse'; export { MDLError, MDLParseError } from './mdoc/errors'; export { VerificationAssessmentId } from './mdoc/checkCallback'; export { getCborEncodeDecodeOptions, setCborEncodeDecodeOptions } from './cbor'; +export { Signer, LocalKeySigner, AzureKeyVaultSigner, AzureKeyVaultSignerConfig } from './mdoc/signing'; diff --git a/src/mdoc/model/Document.ts b/src/mdoc/model/Document.ts index 4ad7d3d..056b7c7 100644 --- a/src/mdoc/model/Document.ts +++ b/src/mdoc/model/Document.ts @@ -6,6 +6,7 @@ import { IssuerSignedItem } from '../IssuerSignedItem'; import IssuerAuth from './IssuerAuth'; import { DeviceKeyInfo, DigestAlgorithm, DocType, IssuerNameSpaces, MSO, SupportedAlgs, ValidityInfo } from './types'; import { IssuerSignedDocument } from './IssuerSignedDocument'; +import { Signer } from '../signing/Signer'; const DEFAULT_NS = 'org.iso.18013.5.1'; @@ -162,14 +163,16 @@ export class Document { * Generate the issuer signature for the document. * * @param {Object} params - The parameters object - * @param {jose.JWK | Uint8Array} params.issuerPrivateKey - The issuer's private key either in JWK format or COSE_KEY format as buffer. + * @param {jose.JWK | Uint8Array} [params.issuerPrivateKey] - The issuer's private key either in JWK format or COSE_KEY format as buffer. Required if signer is not provided. + * @param {Signer} [params.signer] - A Signer implementation for custom signing (e.g., AzureKeyVaultSigner). Required if issuerPrivateKey is not provided. * @param {string | Uint8Array | Array} params.issuerCertificate - The issuer's certificate in pem format, as a buffer, or an array. * @param {SupportedAlgs} params.alg - The algorhitm used for the MSO signature. - * @param {string | Uint8Array} [params.kid] - The key id of the issuer's private key. default: issuerPrivateKey.kid + * @param {string | Uint8Array} [params.kid] - The key id of the issuer's private key. default: issuerPrivateKey.kid or signer.getKeyId() * @returns {Promise} - The signed document */ async sign(params: { - issuerPrivateKey: jose.JWK | Uint8Array, + issuerPrivateKey?: jose.JWK | Uint8Array, + signer?: Signer, issuerCertificate: string | Uint8Array | Array, alg: SupportedAlgs, kid?: string | Uint8Array, @@ -178,6 +181,14 @@ export class Document { throw new Error('No namespaces added'); } + // Validate that either issuerPrivateKey or signer is provided (but not both) + if (!params.issuerPrivateKey && !params.signer) { + throw new Error('Must provide either issuerPrivateKey or signer'); + } + if (params.issuerPrivateKey && params.signer) { + throw new Error('Cannot provide both issuerPrivateKey and signer. Use one or the other.'); + } + let issuerCertificateChain: Uint8Array[]; if (Array.isArray(params.issuerCertificate)) { @@ -188,11 +199,16 @@ export class Document { issuerCertificateChain = [params.issuerCertificate]; } - const issuerPrivateKeyJWK = params.issuerPrivateKey instanceof Uint8Array ? - COSEKeyToJWK(params.issuerPrivateKey) : - params.issuerPrivateKey; + // Prepare key material based on what was provided + let issuerPrivateKeyJWK: jose.JWK | undefined; + let issuerPrivateKey: jose.KeyLike | Uint8Array | undefined; - const issuerPrivateKey = await jose.importJWK(issuerPrivateKeyJWK); + if (params.issuerPrivateKey) { + issuerPrivateKeyJWK = params.issuerPrivateKey instanceof Uint8Array ? + COSEKeyToJWK(params.issuerPrivateKey) : + params.issuerPrivateKey; + issuerPrivateKey = await jose.importJWK(issuerPrivateKeyJWK); + } const valueDigests = new Map(await Promise.all(Object.entries(this.#issuerNameSpaces).map(async ([namespace, items]) => { const digestMap = new Map(); @@ -212,19 +228,38 @@ export class Document { validityInfo: this.#validityInfo, }; - const payload = cborEncode(DataItem.fromData(mso)); + const payload = new Uint8Array(cborEncode(DataItem.fromData(mso))); const protectedHeader: ProtectedHeaders = { alg: params.alg }; + + // Determine kid from params, issuerPrivateKeyJWK, or signer + const { kid: paramKid, signer } = params; + let kid: string | Uint8Array | undefined = paramKid; + if (!kid && issuerPrivateKeyJWK) { + kid = issuerPrivateKeyJWK.kid; + } else if (!kid && signer) { + kid = signer.getKeyId(); + } + const unprotectedHeader: UnprotectedHeaders = { - kid: params.kid ?? issuerPrivateKeyJWK.kid, + kid, x5chain: issuerCertificateChain.length === 1 ? issuerCertificateChain[0] : issuerCertificateChain, }; - const issuerAuth = await IssuerAuth.sign( - protectedHeader, - unprotectedHeader, - payload, - issuerPrivateKey, - ); + // Use either traditional signing or custom signer + const issuerAuth = signer ? + await IssuerAuth.signWithSigner( + protectedHeader, + unprotectedHeader, + payload, + signer, + params.alg, + ) : + await IssuerAuth.sign( + protectedHeader, + unprotectedHeader, + payload, + issuerPrivateKey!, + ); const issuerSigned = { issuerAuth, diff --git a/src/mdoc/model/IssuerAuth.ts b/src/mdoc/model/IssuerAuth.ts index 78ce288..51380d0 100644 --- a/src/mdoc/model/IssuerAuth.ts +++ b/src/mdoc/model/IssuerAuth.ts @@ -1,9 +1,100 @@ import { ProtectedHeaders, Sign1, UnprotectedHeaders } from 'cose-kit'; import { X509Certificate } from '@peculiar/x509'; import { KeyLike } from 'jose'; -import { cborDecode } from '../../cbor'; +import { cborDecode, cborEncode } from '../../cbor'; import { DataItem } from '../../cbor/DataItem'; -import { MSO } from './types'; +import { MSO, SupportedAlgs } from './types'; +import { Signer } from '../signing/Signer'; + +// COSE Header parameter constants +// Reference: https://www.iana.org/assignments/cose/cose.xhtml#header-parameters +const COSE_HEADERS = { + ALG: 1, + CRIT: 2, + CONTENT_TYPE: 3, + KID: 4, + IV: 5, + PARTIAL_IV: 6, + X5CHAIN: 33, +}; + +// COSE Algorithm identifiers +// Reference: https://www.iana.org/assignments/cose/cose.xhtml#algorithms +const COSE_ALG_MAP: Record = { + EdDSA: -8, + ES256: -7, + ES384: -35, + ES512: -36, +}; + +/** + * Convert ProtectedHeaders to a Map for CBOR encoding + */ +function protectedHeadersToMap(headers: ProtectedHeaders): Map { + const map = new Map(); + + if (headers.alg) { + const algValue = COSE_ALG_MAP[headers.alg as SupportedAlgs]; + if (algValue !== undefined) { + map.set(COSE_HEADERS.ALG, algValue); + } + } + + if (headers.crit) { + map.set(COSE_HEADERS.CRIT, headers.crit); + } + + if (headers.ctyp !== undefined) { + map.set(COSE_HEADERS.CONTENT_TYPE, headers.ctyp); + } + + // Handle any custom headers + for (const [key, value] of Object.entries(headers)) { + if (key !== 'alg' && key !== 'crit' && key !== 'ctyp') { + const numKey = typeof key === 'string' ? parseInt(key, 10) : key; + if (!Number.isNaN(numKey)) { + map.set(numKey, value); + } + } + } + + return map; +} + +/** + * Convert UnprotectedHeaders to a Map for COSE structure + */ +function unprotectedHeadersToMap(headers: UnprotectedHeaders | undefined): Map { + const map = new Map(); + + if (!headers) { + return map; + } + + if (headers.ctyp !== undefined) { + map.set(COSE_HEADERS.CONTENT_TYPE, headers.ctyp); + } + + if (headers.kid !== undefined) { + map.set(COSE_HEADERS.KID, headers.kid); + } + + if (headers.x5chain !== undefined) { + map.set(COSE_HEADERS.X5CHAIN, headers.x5chain); + } + + // Handle any custom headers + for (const [key, value] of Object.entries(headers)) { + if (key !== 'ctyp' && key !== 'kid' && key !== 'x5chain') { + const numKey = typeof key === 'string' ? parseInt(key, 10) : key; + if (!Number.isNaN(numKey)) { + map.set(numKey, value); + } + } + } + + return map; +} /** * The IssuerAuth which is a COSE_Sign1 message @@ -72,4 +163,61 @@ export default class IssuerAuth extends Sign1 { sign1.signature, ); } + + /** + * Sign using a Signer interface (e.g., AzureKeyVaultSigner) + * This method manually constructs the COSE_Sign1 structure to support custom signing mechanisms + * + * @param protectedHeaders - The protected headers + * @param unprotectedHeaders - The unprotected headers + * @param payload - The payload to sign + * @param signer - The Signer implementation + * @param alg - The algorithm to use + * @returns The signed IssuerAuth + */ + static async signWithSigner( + protectedHeaders: ProtectedHeaders, + unprotectedHeaders: UnprotectedHeaders | undefined, + payload: Uint8Array, + signer: Signer, + alg: SupportedAlgs, + ): Promise { + // Convert headers to Maps + const protectedHeadersMap = protectedHeadersToMap(protectedHeaders); + + // Ensure alg is set in protected headers + if (!protectedHeadersMap.has(COSE_HEADERS.ALG)) { + protectedHeadersMap.set(COSE_HEADERS.ALG, COSE_ALG_MAP[alg]); + } + + // Encode the protected headers + const encodedProtectedHeaders = cborEncode(protectedHeadersMap); + + // Convert unprotected headers to Map + const unprotectedHeadersMap = unprotectedHeadersToMap(unprotectedHeaders); + + // Construct the Sig_structure as per RFC 8152 Section 4.4 + // Sig_structure = [ + // context: "Signature1", + // protected: serialized_protected_headers, + // external_aad: empty_or_serialized_aad, + // payload: payload + // ] + const toBeSigned = cborEncode([ + 'Signature1', + encodedProtectedHeaders, + new Uint8Array(), // external_aad (empty for normal signing) + payload, + ]); + + // Sign using the custom signer + const signature = await signer.sign(alg, new Uint8Array(toBeSigned)); + + return new IssuerAuth( + new Uint8Array(encodedProtectedHeaders), + unprotectedHeadersMap, + payload, + signature, + ); + } } diff --git a/src/mdoc/signing/AzureKeyVaultSigner.ts b/src/mdoc/signing/AzureKeyVaultSigner.ts new file mode 100644 index 0000000..16d26e9 --- /dev/null +++ b/src/mdoc/signing/AzureKeyVaultSigner.ts @@ -0,0 +1,210 @@ +import * as jose from 'jose'; +import { CryptographyClient, KeyClient, SignatureAlgorithm } from '@azure/keyvault-keys'; +import { TokenCredential, DefaultAzureCredential } from '@azure/identity'; +import { createHash } from 'crypto'; +import { Signer } from './Signer'; + +export interface AzureKeyVaultSignerConfig { + /** + * The Azure Key Vault URL (e.g., 'https://my-vault.vault.azure.net') + */ + keyVaultUrl: string; + + /** + * The name of the key in Azure Key Vault + */ + keyName: string; + + /** + * Optional key version. If not specified, uses the latest version. + */ + keyVersion?: string; + + /** + * Optional Azure credential. If not specified, uses DefaultAzureCredential. + */ + credential?: TokenCredential; +} + +/** + * Azure Key Vault-based signer that uses Azure Key Vault to perform signing operations. + * This allows signing with private keys stored in Azure Key Vault without exposing + * the private key material. + */ +export class AzureKeyVaultSigner implements Signer { + private cryptoClient: CryptographyClient; + private keyClient: KeyClient; + private keyName: string; + private keyVersion?: string; + private credential: TokenCredential; + private keyVaultUrl: string; + private cachedPublicKey?: jose.JWK; + + constructor(config: AzureKeyVaultSignerConfig) { + // Normalize the key vault URL by removing trailing slash + this.keyVaultUrl = config.keyVaultUrl.replace(/\/$/, ''); + this.keyName = config.keyName; + this.keyVersion = config.keyVersion; + this.credential = config.credential || new DefaultAzureCredential(); + + this.keyClient = new KeyClient(this.keyVaultUrl, this.credential); + + // Initialize the cryptography client + // If keyVersion is provided, use it; otherwise, use the key name which will use the + // latest version + const keyId = this.keyVersion + ? `${this.keyVaultUrl}/keys/${this.keyName}/${this.keyVersion}` + : `${this.keyVaultUrl}/keys/${this.keyName}`; + + this.cryptoClient = new CryptographyClient(keyId, this.credential); + } + + async sign(algorithm: string, data: Uint8Array): Promise { + // Azure Key Vault's sign operation expects a digest, not the raw data + // So we need to hash the data first + const digest = this.hashData(algorithm, data); + + // Map COSE algorithm to Azure signature algorithm + const azureAlgorithm = this.mapCOSEtoAzureAlgorithm(algorithm); + + // Perform the signing operation in Azure Key Vault + const signResult = await this.cryptoClient.sign(azureAlgorithm, digest); + + return new Uint8Array(signResult.result); + } + + async getPublicKey(): Promise { + if (this.cachedPublicKey) { + return this.cachedPublicKey; + } + + // Retrieve the key from Azure Key Vault + const keyVaultKey = await this.keyClient.getKey(this.keyName, { version: this.keyVersion }); + + if (!keyVaultKey.key) { + throw new Error(`Failed to retrieve key ${this.keyName} from Azure Key Vault`); + } + + // Convert Azure Key Vault key to JWK format + const jwk = this.azureKeyToJWK(keyVaultKey.key); + + this.cachedPublicKey = jwk; + return jwk; + } + + getKeyId(): string { + return this.keyName; + } + + /** + * Hash the data according to the algorithm + * @param algorithm - COSE algorithm identifier + * @param data - Data to hash + * @returns The digest + */ + private hashData(algorithm: string, data: Uint8Array): Uint8Array { + const hashAlgorithm = this.getHashAlgorithm(algorithm); + const hash = createHash(hashAlgorithm); + hash.update(data); + return new Uint8Array(hash.digest()); + } + + /** + * Get the hash algorithm for a COSE algorithm + * @param coseAlg - COSE algorithm identifier + * @returns Hash algorithm name for Node.js crypto + */ + private getHashAlgorithm(coseAlg: string): string { + const hashMap: Record = { + ES256: 'sha256', + ES384: 'sha384', + ES512: 'sha512', + RS256: 'sha256', + RS384: 'sha384', + RS512: 'sha512', + PS256: 'sha256', + PS384: 'sha384', + PS512: 'sha512', + }; + + const hash = hashMap[coseAlg]; + if (!hash) { + throw new Error(`Unsupported COSE algorithm for hashing: ${coseAlg}`); + } + + return hash; + } + + /** + * Map COSE algorithm to Azure Key Vault signature algorithm + * + * Supported algorithms: + * - ES256 (ECDSA with P-256 curve and SHA-256) + * - ES384 (ECDSA with P-384 curve and SHA-384) + * - ES512 (ECDSA with P-521 curve and SHA-512) + * - RS256, RS384, RS512, PS256, PS384, PS512 (RSA algorithms) + * + * NOT supported: + * - EdDSA (Ed25519) - Azure Key Vault HSM does not support Ed25519 keys + * + * @param coseAlg - COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') + * @returns Azure SignatureAlgorithm + * @throws Error if algorithm is not supported + */ + private mapCOSEtoAzureAlgorithm(coseAlg: string): SignatureAlgorithm { + // EdDSA is explicitly not supported by Azure Key Vault HSM + if (coseAlg === 'EdDSA') { + throw new Error( + 'EdDSA (Ed25519) is not supported by Azure Key Vault HSM. ' + + 'Supported elliptic curve algorithms: ES256, ES384, ES512. ' + + 'Supported RSA algorithms: RS256, RS384, RS512, PS256, PS384, PS512.', + ); + } + + const algorithmMap: Record = { + ES256: 'ES256', + ES384: 'ES384', + ES512: 'ES512', + RS256: 'RS256', + RS384: 'RS384', + RS512: 'RS512', + PS256: 'PS256', + PS384: 'PS384', + PS512: 'PS512', + }; + + const azureAlg = algorithmMap[coseAlg]; + if (!azureAlg) { + throw new Error(`Unsupported COSE algorithm for Azure Key Vault: ${coseAlg}`); + } + + return azureAlg; + } + + /** + * Convert Azure Key Vault key to JWK format + * @param azureKey - Key from Azure Key Vault + * @returns JWK representation of the public key + */ + private azureKeyToJWK(azureKey: any): jose.JWK { + // Azure Key Vault returns keys in JWK format already + // We just need to extract the public key components + const jwk: jose.JWK = { + kty: azureKey.kty.replace('-HSM', ''), // Normalize EC-HSM to EC, RSA-HSM to RSA + kid: azureKey.kid, + }; + + if (azureKey.kty === 'EC' || azureKey.kty === 'EC-HSM') { + jwk.crv = azureKey.crv; + jwk.x = azureKey.x; + jwk.y = azureKey.y; + } else if (azureKey.kty === 'RSA' || azureKey.kty === 'RSA-HSM') { + jwk.n = azureKey.n; + jwk.e = azureKey.e; + } else { + throw new Error(`Unsupported key type: ${azureKey.kty}`); + } + + return jwk; + } +} diff --git a/src/mdoc/signing/LocalKeySigner.ts b/src/mdoc/signing/LocalKeySigner.ts new file mode 100644 index 0000000..afa3805 --- /dev/null +++ b/src/mdoc/signing/LocalKeySigner.ts @@ -0,0 +1,68 @@ +import * as jose from 'jose'; +import { createSign } from 'crypto'; +import { Signer } from './Signer'; + +/** + * Local key-based signer that uses JWK for signing operations. + * This provides backward compatibility with the existing signing mechanism. + */ +export class LocalKeySigner implements Signer { + private jwk: jose.JWK; + + constructor(jwk: jose.JWK) { + this.jwk = jwk; + } + + async sign(algorithm: string, data: Uint8Array): Promise { + // Import the JWK as a KeyLike object + const key = await jose.importJWK(this.jwk); + + // Map COSE algorithm to Node.js digest algorithm + const digestAlgorithm = this.mapCOSEAlgorithmToNodeDigest(algorithm); + + // Use Node.js crypto to sign + const signer = createSign(digestAlgorithm); + signer.update(data); + signer.end(); + + // KeyLike from jose.importJWK is compatible with Node.js crypto + const signature = signer.sign(key as any); + return new Uint8Array(signature); + } + + async getPublicKey(): Promise { + // Remove the private key component to get the public key + const { d, ...publicKey } = this.jwk; + return publicKey; + } + + getKeyId(): string | Uint8Array | undefined { + return this.jwk.kid; + } + + /** + * Map COSE algorithm names to Node.js digest algorithm names + * @param coseAlg - COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') + * @returns Node.js digest algorithm name + */ + private mapCOSEAlgorithmToNodeDigest(coseAlg: string): string { + const algorithmMap: Record = { + ES256: 'sha256', + ES384: 'sha384', + ES512: 'sha512', + RS256: 'RSA-SHA256', + RS384: 'RSA-SHA384', + RS512: 'RSA-SHA512', + PS256: 'RSA-SHA256', + PS384: 'RSA-SHA384', + PS512: 'RSA-SHA512', + }; + + const nodeAlg = algorithmMap[coseAlg]; + if (!nodeAlg) { + throw new Error(`Unsupported COSE algorithm: ${coseAlg}`); + } + + return nodeAlg; + } +} diff --git a/src/mdoc/signing/Signer.ts b/src/mdoc/signing/Signer.ts new file mode 100644 index 0000000..cfc5554 --- /dev/null +++ b/src/mdoc/signing/Signer.ts @@ -0,0 +1,27 @@ +import * as jose from 'jose'; + +/** + * Interface for signing operations that can be implemented by various + * signing mechanisms (local keys, HSMs, KMS, etc.) + */ +export interface Signer { + /** + * Sign data using the configured signing mechanism + * @param algorithm - The COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') + * @param data - The data to sign + * @returns The signature as a Uint8Array + */ + sign(algorithm: string, data: Uint8Array): Promise; + + /** + * Get the public key information for this signer in JWK format + * @returns The public key as a JWK + */ + getPublicKey(): Promise; + + /** + * Get the key ID for this signer + * @returns The key ID as a string, Uint8Array, or undefined + */ + getKeyId(): string | Uint8Array | undefined; +} diff --git a/src/mdoc/signing/index.ts b/src/mdoc/signing/index.ts new file mode 100644 index 0000000..ec3f453 --- /dev/null +++ b/src/mdoc/signing/index.ts @@ -0,0 +1,3 @@ +export { Signer } from './Signer'; +export { LocalKeySigner } from './LocalKeySigner'; +export { AzureKeyVaultSigner, AzureKeyVaultSignerConfig } from './AzureKeyVaultSigner'; From 2b188ac0fe84010e7a18a646c89273c451b840d0 Mon Sep 17 00:00:00 2001 From: Warren Gallagher Date: Tue, 28 Oct 2025 08:14:19 -0400 Subject: [PATCH 2/6] feat: add Signer abstraction interface for pluggable signing implementations Add Example Azure Key Vault integration for hardware-backed cryptographic signing of mobile driver's licenses (mDL) compliant with ISO 18013-5. Key changes: - Introduce Signer abstraction interface for pluggable signing implementations - Implement AzureKeyVaultSigner with @azure/keyvault-keys and @azure/identity - Refactor existing signing logic into LocalKeySigner for backward compatibility - Add comprehensive test suite with certificate chain validation - Support ES256, ES384, ES512 signature algorithms - Document EdDSA/Ed25519 limitation (not supported by Azure Key Vault HSM) - Include setup guide and integration documentation --- __tests__/issuing/azureKeyVault.tests.ts | 429 ------------------ .../AZURE_KEY_VAULT_EXAMPLE.md | 0 .../AZURE_KEY_VAULT_INTEGRATION.md | 0 examples/azure-keyvault-signer/README.md | 128 ++++++ .../__tests__/azureKeyVault.tests.ts | 205 +++++++++ examples/azure-keyvault-signer/package.json | 33 ++ .../azure-keyvault-signer/sample-azure-env.sh | 0 .../src}/AzureKeyVaultSigner.ts | 2 +- package.json | 2 - src/index.ts | 2 +- src/mdoc/signing/index.ts | 1 - 11 files changed, 368 insertions(+), 434 deletions(-) delete mode 100644 __tests__/issuing/azureKeyVault.tests.ts rename AZURE_KEY_VAULT_EXAMPLE.md => examples/azure-keyvault-signer/AZURE_KEY_VAULT_EXAMPLE.md (100%) rename AZURE_KEY_VAULT_INTEGRATION.md => examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md (100%) create mode 100644 examples/azure-keyvault-signer/README.md create mode 100644 examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts create mode 100644 examples/azure-keyvault-signer/package.json rename sample-azure-env.sh => examples/azure-keyvault-signer/sample-azure-env.sh (100%) rename {src/mdoc/signing => examples/azure-keyvault-signer/src}/AzureKeyVaultSigner.ts (99%) diff --git a/__tests__/issuing/azureKeyVault.tests.ts b/__tests__/issuing/azureKeyVault.tests.ts deleted file mode 100644 index 39bc228..0000000 --- a/__tests__/issuing/azureKeyVault.tests.ts +++ /dev/null @@ -1,429 +0,0 @@ -import * as jose from 'jose'; -import { COSEKeyToJWK } from 'cose-kit'; -import { ClientSecretCredential } from '@azure/identity'; -import { - MDoc, - Document, - Verifier, - parse, - IssuerSignedDocument, - AzureKeyVaultSigner, -} from '../../src'; -import { DEVICE_JWK, ISSUER_CERTIFICATE } from './config'; - -const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; - -// Azure Key Vault certificate chain (leaf + intermediate + root) -// Full chain for embedding in the x5chain header -const AZURE_KEY_VAULT_CERTIFICATE_CHAIN = `-----BEGIN CERTIFICATE----- -MIID1jCCA3ygAwIBAgIUddQy2FOR8KrfEaXGToyfRw+6VYkwCgYIKoZIzj0EAwIw -gYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T -YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT -ZWN1cml0eTEiMCAGA1UEAwwZTXlDb21wYW55IEludGVybWVkaWF0ZSBDQTAeFw0y -NTEwMjcxNDIyNDJaFw0yNjEwMjcxNDIyNDJaMHAxCzAJBgNVBAYTAlVTMRMwEQYD -VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRYwFAYDVQQK -DA1NeUNvbXBhbnkgSW5jMRwwGgYDVQQDDBNsZWFmMS5teWNvbXBhbnkuY29tMFkw -EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQWHhjGbA4ZZEZasF62pYctQ8tHX0HMDJ -Jb0QgiTaq76b+FMZWuQ/SU2wIV9K4JfjVbLWU4JuyQO+xr5tKoA+vqOCAdgwggHU -MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUF -BwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUatgdVmySrerOBDjCpM4Og9A6hHowHwYD -VR0jBBgwFoAUhYS70VQN/x4zszXTuFXnmlwuD6YwcgYDVR0RBGswaYITbGVhZjEu -bXljb21wYW55LmNvbYIXd3d3LmxlYWYxLm15Y29tcGFueS5jb22CFSoubGVhZjEu -bXljb21wYW55LmNvbYcEwKgBZIYcaHR0cHM6Ly9sZWFmMS5teWNvbXBhbnkuY29t -LzB6BgNVHR8EczBxMDOgMaAvhi1odHRwOi8vY3JsLm15Y29tcGFueS5jb20vbXlj -b21wYW55LWludC1jYS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwtYmFja3VwLm15Y29t -cGFueS5jb20vbXljb21wYW55LWludC1jYS5jcmwwZQYIKwYBBQUHAQEEWTBXMCUG -CCsGAQUFBzABhhlodHRwOi8vb2NzcC5teWNvbXBhbnkuY29tMC4GCCsGAQUFBzAC -hiJodHRwOi8vY2EubXljb21wYW55LmNvbS9pbnQtY2EuY3J0MAoGCCqGSM49BAMC -A0gAMEUCIBACN4AUAB8y360eWaX5i6dZ7VL+MOQNm/YSUcozrWYTAiEArvMCGoep -W8VOY3Z3NNLlvdJFX8WzgV4RjBClXUcNjQA= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIID1jCCA3ugAwIBAgIUM51uNu3ULLYO71dDQK25v/Qr/sEwCgYIKoZIzj0EAwIw -gYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T -YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT -ZWN1cml0eTEaMBgGA1UEAwwRTXlDb21wYW55IFJvb3QgQ0EwHhcNMjUxMDI3MTQy -MjQwWhcNMzAxMDI2MTQyMjQwWjCBiTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh -bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoMDU15Q29t -cGFueSBJbmMxETAPBgNVBAsMCFNlY3VyaXR5MSIwIAYDVQQDDBlNeUNvbXBhbnkg -SW50ZXJtZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKZ7u2UVX -i34ILZhRc6Y2xgcLLV7V7uaIdry3zWIGerJRYBl9qc5QE+c6vhUoLr3uXJ8Ypsc5 -axT88I+s40HcgaOCAcUwggHBMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBSFhLvRVA3/HjOzNdO4VeeaXC4PpjAfBgNVHSMEGDAW -gBQIYDTeNr2PmcPzVVaVPsyAZCSRijB4BgNVHREEcTBvghRpbnQtY2EubXljb21w -YW55LmNvbYIdaW50ZXJtZWRpYXRlLWNhLm15Y29tcGFueS5jb22GHGh0dHA6Ly9p -bnQtY2EubXljb21wYW55LmNvbS+BGmludC1jYS1hZG1pbkBteWNvbXBhbnkuY29t -MHoGA1UdHwRzMHEwM6AxoC+GLWh0dHA6Ly9jcmwubXljb21wYW55LmNvbS9teWNv -bXBhbnktaW50LWNhLmNybDA6oDigNoY0aHR0cDovL2NybC1iYWNrdXAubXljb21w -YW55LmNvbS9teWNvbXBhbnktaW50LWNhLmNybDBlBggrBgEFBQcBAQRZMFcwJQYI -KwYBBQUHMAGGGWh0dHA6Ly9vY3NwLm15Y29tcGFueS5jb20wLgYIKwYBBQUHMAKG -Imh0dHA6Ly9jYS5teWNvbXBhbnkuY29tL2ludC1jYS5jcnQwCgYIKoZIzj0EAwID -SQAwRgIhAJl5fDVmA9zRVvPIXVDLeHD0YL8uoU13EEFKN/GcvC52AiEA2jdtwrNR -tgWbVSREsxsK/9ybfUyaUhCcg1jlrwutg7E= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDvDCCA2KgAwIBAgIUe/TFUO8Fc+Ws6vLcZSOBIf09PbQwCgYIKoZIzj0EAwIw -gYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T -YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT -ZWN1cml0eTEaMBgGA1UEAwwRTXlDb21wYW55IFJvb3QgQ0EwHhcNMjUxMDI3MTQy -MjM5WhcNMzUxMDI1MTQyMjM5WjCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh -bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoMDU15Q29t -cGFueSBJbmMxETAPBgNVBAsMCFNlY3VyaXR5MRowGAYDVQQDDBFNeUNvbXBhbnkg -Um9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGbH9E6FoEc7JulkOTFW -/opse0bQtM+rVBxiZkrE4RQ1XYgIfT/SDERX/HbRHsvfTqzPc9i2ICnP1yJsfx0W -FiSjggG0MIIBsDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUCGA03ja9j5nD81VWlT7MgGQkkYowHwYDVR0jBBgwFoAUCGA03ja9 -j5nD81VWlT7MgGQkkYowZAYDVR0RBF0wW4IQY2EubXljb21wYW55LmNvbYIVcm9v -dC1jYS5teWNvbXBhbnkuY29thhhodHRwOi8vY2EubXljb21wYW55LmNvbS+BFmNh -LWFkbWluQG15Y29tcGFueS5jb20wfAYDVR0fBHUwczA0oDKgMIYuaHR0cDovL2Ny -bC5teWNvbXBhbnkuY29tL215Y29tcGFueS1yb290LWNhLmNybDA7oDmgN4Y1aHR0 -cDovL2NybC1iYWNrdXAubXljb21wYW55LmNvbS9teWNvbXBhbnktcm9vdC1jYS5j -cmwwZgYIKwYBBQUHAQEEWjBYMCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5teWNv -bXBhbnkuY29tMC8GCCsGAQUFBzAChiNodHRwOi8vY2EubXljb21wYW55LmNvbS9y -b290LWNhLmNydDAKBggqhkjOPQQDAgNIADBFAiEA/U11iSXRvzPfriPTkJIX+zgc -+55+glglVUTF81akovUCIFvoXh/jh+fanzVNEIOeLMdUzP7ZeSIwlbjpbSVOGjWM ------END CERTIFICATE-----`; - -// Root CA certificate for verification -const AZURE_KEY_VAULT_ROOT_CERTIFICATE = `-----BEGIN CERTIFICATE----- -MIIDvDCCA2KgAwIBAgIUe/TFUO8Fc+Ws6vLcZSOBIf09PbQwCgYIKoZIzj0EAwIw -gYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T -YW4gRnJhbmNpc2NvMRYwFAYDVQQKDA1NeUNvbXBhbnkgSW5jMREwDwYDVQQLDAhT -ZWN1cml0eTEaMBgGA1UEAwwRTXlDb21wYW55IFJvb3QgQ0EwHhcNMjUxMDI3MTQy -MjM5WhcNMzUxMDI1MTQyMjM5WjCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh -bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoMDU15Q29t -cGFueSBJbmMxETAPBgNVBAsMCFNlY3VyaXR5MRowGAYDVQQDDBFNeUNvbXBhbnkg -Um9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGbH9E6FoEc7JulkOTFW -/opse0bQtM+rVBxiZkrE4RQ1XYgIfT/SDERX/HbRHsvfTqzPc9i2ICnP1yJsfx0W -FiSjggG0MIIBsDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUCGA03ja9j5nD81VWlT7MgGQkkYowHwYDVR0jBBgwFoAUCGA03ja9 -j5nD81VWlT7MgGQkkYowZAYDVR0RBF0wW4IQY2EubXljb21wYW55LmNvbYIVcm9v -dC1jYS5teWNvbXBhbnkuY29thhhodHRwOi8vY2EubXljb21wYW55LmNvbS+BFmNh -LWFkbWluQG15Y29tcGFueS5jb20wfAYDVR0fBHUwczA0oDKgMIYuaHR0cDovL2Ny -bC5teWNvbXBhbnkuY29tL215Y29tcGFueS1yb290LWNhLmNybDA7oDmgN4Y1aHR0 -cDovL2NybC1iYWNrdXAubXljb21wYW55LmNvbS9teWNvbXBhbnktcm9vdC1jYS5j -cmwwZgYIKwYBBQUHAQEEWjBYMCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5teWNv -bXBhbnkuY29tMC8GCCsGAQUFBzAChiNodHRwOi8vY2EubXljb21wYW55LmNvbS9y -b290LWNhLmNydDAKBggqhkjOPQQDAgNIADBFAiEA/U11iSXRvzPfriPTkJIX+zgc -+55+glglVUTF81akovUCIFvoXh/jh+fanzVNEIOeLMdUzP7ZeSIwlbjpbSVOGjWM ------END CERTIFICATE-----`; - -// Check if Azure environment variables are set -const isAzureConfigured = () => { - return !!( - process.env.AZURE_KEYVAULT_URL && - process.env.AZURE_TENANT_ID && - process.env.AZURE_CLIENT_ID && - process.env.AZURE_CLIENT_SECRET - ); -}; - -// Helper to get Azure configuration -const getAzureConfig = () => { - if (!isAzureConfigured()) { - return null; - } - - return { - keyVaultUrl: process.env.AZURE_KEYVAULT_URL!, - tenantId: process.env.AZURE_TENANT_ID!, - clientId: process.env.AZURE_CLIENT_ID!, - clientSecret: process.env.AZURE_CLIENT_SECRET!, - keyName: 'leaf-1-ec-hsm-key', // The HSM-managed ES256 key - }; -}; - -// Conditional test suite -const describeIfAzureConfigured = isAzureConfigured() ? describe : describe.skip; - -describeIfAzureConfigured('Azure Key Vault integration', () => { - let azureConfig: ReturnType; - let signer: AzureKeyVaultSigner; - - // Get config inside beforeAll to avoid evaluation when tests are skipped - beforeAll(() => { - azureConfig = getAzureConfig(); - if (!azureConfig) { - throw new Error('Azure configuration not available'); - } - }); - - beforeAll(() => { - if (!azureConfig) return; - - // Create Azure credential - const credential = new ClientSecretCredential( - azureConfig.tenantId, - azureConfig.clientId, - azureConfig.clientSecret, - ); - - // Create the signer - signer = new AzureKeyVaultSigner({ - keyVaultUrl: azureConfig.keyVaultUrl, - keyName: azureConfig.keyName, - credential, - }); - }); - - describe('AzureKeyVaultSigner', () => { - it('should initialize successfully', () => { - expect(signer).toBeDefined(); - expect(signer.getKeyId()).toBe(azureConfig!.keyName); - }); - - it('should retrieve the public key from Azure Key Vault', async () => { - const publicKey = await signer.getPublicKey(); - expect(publicKey).toBeDefined(); - expect(publicKey.kty).toBe('EC'); - expect(publicKey.crv).toBe('P-256'); // ES256 uses P-256 - expect(publicKey.x).toBeDefined(); - expect(publicKey.y).toBeDefined(); - expect(publicKey.d).toBeUndefined(); // Should not have private key component - }, 10000); // 10 second timeout for network call - - it('should sign data using Azure Key Vault', async () => { - const testData = new Uint8Array([1, 2, 3, 4, 5]); - const signature = await signer.sign('ES256', testData); - - expect(signature).toBeDefined(); - expect(signature).toBeInstanceOf(Uint8Array); - expect(signature.length).toBeGreaterThan(0); - // ES256 signatures are typically 64 bytes (DER encoded can be slightly longer) - expect(signature.length).toBeGreaterThanOrEqual(64); - }, 10000); - }); - - describe('issuing an MDOC with Azure Key Vault', () => { - let encoded: Uint8Array; - let parsedDocument: IssuerSignedDocument; - - // Use dates within the certificate validity period (Oct 27, 2025 - Oct 27, 2026) - // Truncate to seconds to match CBOR encoding - const now = new Date(); - now.setMilliseconds(0); - const signed = new Date(now); - signed.setHours(signed.getHours() - 1); // Signed 1 hour ago - const validFrom = new Date(signed); - validFrom.setMinutes(signed.getMinutes() - 10); // Valid from 10 minutes before signing - const validUntil = new Date(signed); - validUntil.setMonth(signed.getMonth() + 6); // 6 months validity - const expectedUpdate = new Date(signed); - expectedUpdate.setMonth(signed.getMonth() + 1); // Update in 1 month - - beforeAll(async () => { - const document = await new Document('org.iso.18013.5.1.mDL') - .addIssuerNameSpace('org.iso.18013.5.1', { - family_name: 'Smith', - given_name: 'Alice', - birth_date: '1990-05-15', - issue_date: '2024-01-15', - expiry_date: '2034-01-15', - issuing_country: 'US', - issuing_authority: 'CA DMV', - document_number: 'AKV123456', - portrait: 'bstr', - driving_privileges: [ - { - vehicle_category_code: 'C', - issue_date: '2024-01-15', - expiry_date: '2034-01-15', - }, - ], - }) - .useDigestAlgorithm('SHA-256') - .addValidityInfo({ - signed, - validFrom, - validUntil, - expectedUpdate, - }) - .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) - .sign({ - signer, // Use Azure Key Vault signer - issuerCertificate: AZURE_KEY_VAULT_CERTIFICATE_CHAIN, - alg: 'ES256', - }); - - const mdoc = new MDoc([document]); - const encodedBuffer = mdoc.encode(); - encoded = new Uint8Array(encodedBuffer); - - const parsedMDOC = parse(encoded); - [parsedDocument] = parsedMDOC.documents; - }, 15000); // 15 second timeout for signing operation - - it('should create a valid signed document', () => { - expect(encoded).toBeDefined(); - expect(encoded).toBeInstanceOf(Uint8Array); - expect(encoded.length).toBeGreaterThan(0); - }); - - it('should be verifiable with the matching Azure Key Vault certificate', async () => { - // Full cryptographic signature verification with matching certificate - const verifier = new Verifier([AZURE_KEY_VAULT_ROOT_CERTIFICATE]); - await verifier.verify(encoded, { - onCheck: (verification, original) => { - // Skip device auth verification since we're only testing issuer signature - if (verification.category === 'DEVICE_AUTH') { - return; - } - original(verification); - }, - }); - - // Verify structure as well - expect(parsedDocument).toBeDefined(); - expect(parsedDocument.issuerSigned).toBeDefined(); - expect(parsedDocument.issuerSigned.issuerAuth).toBeDefined(); - expect(parsedDocument.issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); - expect(parsedDocument.issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); - }); - - it('should contain the correct validity info', () => { - const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; - expect(validityInfo).toBeDefined(); - // Dates should match exactly since we truncated milliseconds - expect(validityInfo.signed).toEqual(signed); - expect(validityInfo.validFrom).toEqual(validFrom); - expect(validityInfo.validUntil).toEqual(validUntil); - expect(validityInfo.expectedUpdate).toBeDefined(); - expect(validityInfo.expectedUpdate).toEqual(expectedUpdate); - }); - - it('should use the correct digest algorithm', () => { - const { digestAlgorithm } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; - expect(digestAlgorithm).toEqual('SHA-256'); - }); - - it('should include the device public key', () => { - const { deviceKeyInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; - expect(deviceKeyInfo?.deviceKey).toBeDefined(); - const actual = typeof deviceKeyInfo !== 'undefined' && - COSEKeyToJWK(deviceKeyInfo.deviceKey); - expect(actual).toEqual(publicKeyJWK); - }); - - it('should include the namespace and attributes', () => { - const attrValues = parsedDocument.getIssuerNameSpace('org.iso.18013.5.1'); - expect(attrValues).toBeDefined(); - expect(attrValues.family_name).toBe('Smith'); - expect(attrValues.given_name).toBe('Alice'); - expect(attrValues.birth_date.toString()).toBe('1990-05-15'); - expect(attrValues.document_number).toBe('AKV123456'); - expect(attrValues.issuing_country).toBe('US'); - expect(attrValues.issuing_authority).toBe('CA DMV'); - }); - - it('should have a valid COSE signature structure', () => { - const { issuerAuth } = parsedDocument.issuerSigned; - expect(issuerAuth).toBeDefined(); - expect(issuerAuth.payload).toBeDefined(); - expect(issuerAuth.signature).toBeDefined(); - expect(issuerAuth.signature.length).toBeGreaterThan(0); - }); - }); - - describe('comparison with local key signing', () => { - it('both should produce valid COSE structures', async () => { - // Sign with Azure Key Vault - const azureDoc = await new Document('org.iso.18013.5.1.mDL') - .addIssuerNameSpace('org.iso.18013.5.1', { - family_name: 'Test', - given_name: 'Azure', - birth_date: '1990-01-01', - issue_date: '2024-01-01', - expiry_date: '2034-01-01', - issuing_country: 'US', - document_number: 'AZ001', - }) - .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) - .sign({ - signer, - issuerCertificate: AZURE_KEY_VAULT_CERTIFICATE_CHAIN, - alg: 'ES256', - }); - - // Sign with local key (traditional method) - const localDoc = await new Document('org.iso.18013.5.1.mDL') - .addIssuerNameSpace('org.iso.18013.5.1', { - family_name: 'Test', - given_name: 'Local', - birth_date: '1990-01-01', - issue_date: '2024-01-01', - expiry_date: '2034-01-01', - issuing_country: 'US', - document_number: 'LOC001', - }) - .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) - .sign({ - issuerPrivateKey: { - kty: 'EC', - kid: '1234', - x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', - y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', - crv: 'P-256', - d: 'o6PrzBm1dCfSwqJHW6DVqmJOCQSIAosrCPfbFJDMNp4', - }, - issuerCertificate: ISSUER_CERTIFICATE, - alg: 'ES256', - }); - - // Both should encode successfully - const azureEncoded = new Uint8Array(new MDoc([azureDoc]).encode()); - const localEncoded = new Uint8Array(new MDoc([localDoc]).encode()); - - expect(azureEncoded).toBeDefined(); - expect(localEncoded).toBeDefined(); - - // Both should be parseable - const parsedAzure = parse(azureEncoded); - const parsedLocal = parse(localEncoded); - - expect(parsedAzure.documents).toHaveLength(1); - expect(parsedLocal.documents).toHaveLength(1); - - // Local doc should be verifiable with its matching certificate - const localVerifier = new Verifier([ISSUER_CERTIFICATE]); - await localVerifier.verify(localEncoded, { - onCheck: (v, o) => (v.category === 'DEVICE_AUTH' ? undefined : o(v)), - }); - - // Azure doc should also be verifiable with its matching certificate - const azureVerifier = new Verifier([AZURE_KEY_VAULT_ROOT_CERTIFICATE]); - await azureVerifier.verify(azureEncoded, { - onCheck: (v, o) => (v.category === 'DEVICE_AUTH' ? undefined : o(v)), - }); - - // Both signatures should be valid - expect(parsedAzure.documents[0].issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); - expect(parsedAzure.documents[0].issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); - expect(parsedLocal.documents[0].issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); - expect(parsedLocal.documents[0].issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); - }, 15000); - }); -}); - -// Always run this describe block to show helpful message when Azure is not configured -describe('Azure Key Vault integration (configuration check)', () => { - if (!isAzureConfigured()) { - it('should skip tests when Azure environment is not configured', () => { - console.log('\n⚠️ Azure Key Vault tests skipped'); - console.log('To enable these tests, source the azure-env.sh file:'); - console.log(' source azure-env.sh'); - console.log(' npm test'); - console.log('\nRequired environment variables:'); - console.log(' - AZURE_KEYVAULT_URL'); - console.log(' - AZURE_TENANT_ID'); - console.log(' - AZURE_CLIENT_ID'); - console.log(' - AZURE_CLIENT_SECRET\n'); - }); - } else { - it('Azure environment is configured', () => { - const config = getAzureConfig(); - expect(config).toBeDefined(); - expect(config?.keyVaultUrl).toBeDefined(); - expect(config?.keyName).toBe('leaf-1-ec-hsm-key'); - }); - } -}); diff --git a/AZURE_KEY_VAULT_EXAMPLE.md b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_EXAMPLE.md similarity index 100% rename from AZURE_KEY_VAULT_EXAMPLE.md rename to examples/azure-keyvault-signer/AZURE_KEY_VAULT_EXAMPLE.md diff --git a/AZURE_KEY_VAULT_INTEGRATION.md b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md similarity index 100% rename from AZURE_KEY_VAULT_INTEGRATION.md rename to examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md diff --git a/examples/azure-keyvault-signer/README.md b/examples/azure-keyvault-signer/README.md new file mode 100644 index 0000000..1cc9aa8 --- /dev/null +++ b/examples/azure-keyvault-signer/README.md @@ -0,0 +1,128 @@ +# Azure Key Vault Signer Example + +This example demonstrates how to implement a custom `Signer` for the `@auth0/mdl` library using +Azure Key Vault HSM for cryptographic signing operations. + +## Overview + +This implementation shows how to: +- Implement the `Signer` interface from `@auth0/mdl` +- Integrate with Azure Key Vault HSM for signing operations +- Sign mDL/MDOC documents without exposing private key material +- Use Azure Identity for authentication + +## Files + +- `src/AzureKeyVaultSigner.ts` - Implementation of the Signer interface for Azure Key Vault +- `__tests__/azureKeyVault.tests.ts` - Example tests demonstrating usage +- `AZURE_KEY_VAULT_EXAMPLE.md` - Detailed usage examples +- `AZURE_KEY_VAULT_INTEGRATION.md` - Technical architecture documentation +- `sample-azure-env.sh` - Template for Azure environment variables + +## Prerequisites + +1. An Azure Key Vault instance +2. An EC (Elliptic Curve) key stored in Azure Key Vault (ES256, ES384, or ES512) +3. Appropriate Azure credentials configured + +## Installation + +```bash +# Install the core library +npm install @auth0/mdl + +# Install Azure dependencies for this example +npm install @azure/keyvault-keys @azure/identity +``` + +## Quick Start + +```typescript +import { Document } from '@auth0/mdl'; +import { AzureKeyVaultSigner } from './path/to/AzureKeyVaultSigner'; + +// Create an Azure Key Vault signer +const signer = new AzureKeyVaultSigner({ + keyVaultUrl: 'https://my-vault.vault.azure.net', + keyName: 'my-signing-key', +}); + +// Create and sign a document +const doc = new Document('org.iso.18013.5.1.mDL'); + +doc.addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Doe', + given_name: 'John', + birth_date: '1990-01-01', + // ... other attributes +}); + +// Sign using Azure Key Vault +const signedDoc = await doc.sign({ + signer, + issuerCertificate: certificatePEM, + alg: 'ES256', +}); +``` + +## Supported Algorithms + +Azure Key Vault HSM supports the following algorithms: + +- **ES256** - ECDSA using P-256 and SHA-256 ✅ +- **ES384** - ECDSA using P-384 and SHA-384 ✅ +- **ES512** - ECDSA using P-521 and SHA-512 ✅ + +**Not Supported:** +- **EdDSA** (Ed25519) ❌ - Azure Key Vault HSM does not support Ed25519 keys. Use + `LocalKeySigner` for EdDSA. + +## Running the Tests + +1. Copy `sample-azure-env.sh` to `azure-env.sh` +2. Fill in your Azure Key Vault credentials +3. Source the environment file: `source azure-env.sh` +4. Run tests from the root of the repository: `npm test` + +The tests will automatically skip if Azure credentials are not configured. + +## Documentation + +- See [AZURE_KEY_VAULT_EXAMPLE.md](./AZURE_KEY_VAULT_EXAMPLE.md) for detailed usage examples +- See [AZURE_KEY_VAULT_INTEGRATION.md](./AZURE_KEY_VAULT_INTEGRATION.md) for technical + architecture details + +## Implementing Your Own Signer + +This example can be used as a template for implementing other signing backends: + +1. Implement the `Signer` interface from `@auth0/mdl`: + ```typescript + interface Signer { + sign(algorithm: string, data: Uint8Array): Promise; + getPublicKey(): Promise; + getKeyId(): string; + } + ``` + +2. Handle algorithm-specific signing logic +3. Return signatures in the correct format +4. Provide proper error handling + +Other potential implementations: +- AWS KMS Signer +- Google Cloud KMS Signer +- Hardware Security Module (HSM) Signer +- Custom enterprise signing infrastructure + +## Benefits of External Signing + +1. **Security** - Private keys never leave the HSM/secure environment +2. **Compliance** - Meets regulatory requirements (FIPS 140-2, etc.) +3. **Audit Trail** - All operations logged in cloud provider's monitoring +4. **Key Rotation** - Easily rotate keys without changing application code +5. **Access Control** - Fine-grained permissions via cloud IAM + +## License + +This example follows the same license as the @auth0/mdl library (Apache-2.0). diff --git a/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts new file mode 100644 index 0000000..71202bc --- /dev/null +++ b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts @@ -0,0 +1,205 @@ +import * as jose from 'jose'; +import { COSEKeyToJWK } from 'cose-kit'; +import { ClientSecretCredential } from '@azure/identity'; +import { + MDoc, + Document, + parse, + IssuerSignedDocument, +} from '../../../src'; +import { AzureKeyVaultSigner } from '../src/AzureKeyVaultSigner'; +import { DEVICE_JWK, ISSUER_CERTIFICATE } from '../../../__tests__/issuing/config'; + +const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; + +// Check if Azure environment variables are set +const isAzureConfigured = () => { + return !!( + process.env.AZURE_KEYVAULT_URL && + process.env.AZURE_TENANT_ID && + process.env.AZURE_CLIENT_ID && + process.env.AZURE_CLIENT_SECRET + ); +}; + +// Helper to get Azure configuration +const getAzureConfig = () => { + if (!isAzureConfigured()) { + return null; + } + + return { + keyVaultUrl: process.env.AZURE_KEYVAULT_URL!, + tenantId: process.env.AZURE_TENANT_ID!, + clientId: process.env.AZURE_CLIENT_ID!, + clientSecret: process.env.AZURE_CLIENT_SECRET!, + keyName: 'leaf-1-ec-hsm-key', // The HSM-managed ES256 key + }; +}; + +// Conditional test suite +const describeIfAzureConfigured = isAzureConfigured() ? describe : describe.skip; + +describeIfAzureConfigured('Azure Key Vault Signer Example', () => { + let azureConfig: ReturnType; + let signer: AzureKeyVaultSigner; + + // Get config inside beforeAll to avoid evaluation when tests are skipped + beforeAll(() => { + azureConfig = getAzureConfig(); + if (!azureConfig) { + throw new Error('Azure configuration not available'); + } + }); + + beforeAll(() => { + if (!azureConfig) return; + + // Create Azure credential + const credential = new ClientSecretCredential( + azureConfig.tenantId, + azureConfig.clientId, + azureConfig.clientSecret, + ); + + // Create the signer + signer = new AzureKeyVaultSigner({ + keyVaultUrl: azureConfig.keyVaultUrl, + keyName: azureConfig.keyName, + credential, + }); + }); + + describe('AzureKeyVaultSigner', () => { + it('should initialize successfully', () => { + expect(signer).toBeDefined(); + expect(signer.getKeyId()).toBe(azureConfig!.keyName); + }); + + it('should retrieve the public key from Azure Key Vault', async () => { + const publicKey = await signer.getPublicKey(); + expect(publicKey).toBeDefined(); + expect(publicKey.kty).toBe('EC'); + expect(publicKey.crv).toBe('P-256'); // ES256 uses P-256 + expect(publicKey.x).toBeDefined(); + expect(publicKey.y).toBeDefined(); + expect(publicKey.d).toBeUndefined(); // Should not have private key component + }, 10000); // 10 second timeout for network call + + it('should sign data using Azure Key Vault', async () => { + const testData = new Uint8Array([1, 2, 3, 4, 5]); + const signature = await signer.sign('ES256', testData); + + expect(signature).toBeDefined(); + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBeGreaterThan(0); + // ES256 signatures are typically 64 bytes (DER encoded can be slightly longer) + expect(signature.length).toBeGreaterThanOrEqual(64); + }, 10000); + }); + + describe('issuing an MDOC with Azure Key Vault', () => { + let encoded: Uint8Array; + let parsedDocument: IssuerSignedDocument; + + beforeAll(async () => { + const document = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Smith', + given_name: 'Alice', + birth_date: '1990-05-15', + issue_date: '2024-01-15', + expiry_date: '2034-01-15', + issuing_country: 'US', + issuing_authority: 'CA DMV', + document_number: 'AKV123456', + portrait: 'bstr', + driving_privileges: [ + { + vehicle_category_code: 'C', + issue_date: '2024-01-15', + expiry_date: '2034-01-15', + }, + ], + }) + .useDigestAlgorithm('SHA-256') + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + signer, // Use Azure Key Vault signer + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + }); + + const mdoc = new MDoc([document]); + const encodedBuffer = mdoc.encode(); + encoded = new Uint8Array(encodedBuffer); + + const parsedMDOC = parse(encoded); + [parsedDocument] = parsedMDOC.documents; + }, 15000); // 15 second timeout for signing operation + + it('should create a valid signed document', () => { + expect(encoded).toBeDefined(); + expect(encoded).toBeInstanceOf(Uint8Array); + expect(encoded.length).toBeGreaterThan(0); + }); + + it('should have a valid COSE signature structure', () => { + expect(parsedDocument).toBeDefined(); + expect(parsedDocument.issuerSigned).toBeDefined(); + expect(parsedDocument.issuerSigned.issuerAuth).toBeDefined(); + expect(parsedDocument.issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); + expect(parsedDocument.issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); + }); + + it('should use the correct digest algorithm', () => { + const { digestAlgorithm } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(digestAlgorithm).toEqual('SHA-256'); + }); + + it('should include the device public key', () => { + const { deviceKeyInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(deviceKeyInfo?.deviceKey).toBeDefined(); + const actual = typeof deviceKeyInfo !== 'undefined' && + COSEKeyToJWK(deviceKeyInfo.deviceKey); + expect(actual).toEqual(publicKeyJWK); + }); + + it('should include the namespace and attributes', () => { + const attrValues = parsedDocument.getIssuerNameSpace('org.iso.18013.5.1'); + expect(attrValues).toBeDefined(); + expect(attrValues.family_name).toBe('Smith'); + expect(attrValues.given_name).toBe('Alice'); + expect(attrValues.birth_date.toString()).toBe('1990-05-15'); + expect(attrValues.document_number).toBe('AKV123456'); + expect(attrValues.issuing_country).toBe('US'); + expect(attrValues.issuing_authority).toBe('CA DMV'); + }); + }); +}); + +// Always run this describe block to show helpful message when Azure is not configured +describe('Azure Key Vault Signer Example (configuration check)', () => { + if (!isAzureConfigured()) { + it('should skip tests when Azure environment is not configured', () => { + console.log('\n⚠️ Azure Key Vault Signer example tests skipped'); + console.log('To enable these tests, set up your Azure environment:'); + console.log(' 1. Copy examples/azure-keyvault-signer/sample-azure-env.sh'); + console.log(' 2. Fill in your Azure Key Vault credentials'); + console.log(' 3. source azure-env.sh'); + console.log(' 4. npm test'); + console.log('\nRequired environment variables:'); + console.log(' - AZURE_KEYVAULT_URL'); + console.log(' - AZURE_TENANT_ID'); + console.log(' - AZURE_CLIENT_ID'); + console.log(' - AZURE_CLIENT_SECRET\n'); + }); + } else { + it('Azure environment is configured', () => { + const config = getAzureConfig(); + expect(config).toBeDefined(); + expect(config?.keyVaultUrl).toBeDefined(); + expect(config?.keyName).toBe('leaf-1-ec-hsm-key'); + }); + } +}); diff --git a/examples/azure-keyvault-signer/package.json b/examples/azure-keyvault-signer/package.json new file mode 100644 index 0000000..956a165 --- /dev/null +++ b/examples/azure-keyvault-signer/package.json @@ -0,0 +1,33 @@ +{ + "name": "@auth0/mdl-azure-keyvault-signer-example", + "version": "1.0.0", + "description": "Example implementation of Azure Key Vault signer for @auth0/mdl", + "private": true, + "main": "src/AzureKeyVaultSigner.ts", + "scripts": { + "test": "jest --testMatch='**/examples/azure-keyvault-signer/__tests__/**/*.tests.ts'" + }, + "keywords": [ + "auth0", + "mdl", + "azure", + "key-vault", + "hsm", + "signing", + "example" + ], + "author": { + "name": "AffinitiQuest", + "url": "https://affinitiquest.io" + }, + "license": "Apache-2.0", + "peerDependencies": { + "@auth0/mdl": "^1.5.0", + "@azure/identity": "^4.0.0", + "@azure/keyvault-keys": "^4.8.0" + }, + "devDependencies": { + "@azure/identity": "^4.0.0", + "@azure/keyvault-keys": "^4.8.0" + } +} diff --git a/sample-azure-env.sh b/examples/azure-keyvault-signer/sample-azure-env.sh similarity index 100% rename from sample-azure-env.sh rename to examples/azure-keyvault-signer/sample-azure-env.sh diff --git a/src/mdoc/signing/AzureKeyVaultSigner.ts b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts similarity index 99% rename from src/mdoc/signing/AzureKeyVaultSigner.ts rename to examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts index 16d26e9..a6451b0 100644 --- a/src/mdoc/signing/AzureKeyVaultSigner.ts +++ b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts @@ -2,7 +2,7 @@ import * as jose from 'jose'; import { CryptographyClient, KeyClient, SignatureAlgorithm } from '@azure/keyvault-keys'; import { TokenCredential, DefaultAzureCredential } from '@azure/identity'; import { createHash } from 'crypto'; -import { Signer } from './Signer'; +import { Signer } from '../../../src/mdoc/signing/Signer'; export interface AzureKeyVaultSignerConfig { /** diff --git a/package.json b/package.json index 9f5f7ba..f402c26 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,6 @@ "typescript": "^5.1.6" }, "dependencies": { - "@azure/identity": "^4.0.0", - "@azure/keyvault-keys": "^4.8.0", "@noble/curves": "^1.2.0", "@panva/hkdf": "^1.1.1", "@peculiar/x509": "^1.9.5", diff --git a/src/index.ts b/src/index.ts index 81b3060..bf464b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,4 +10,4 @@ export { DeviceResponse } from './mdoc/model/DeviceResponse'; export { MDLError, MDLParseError } from './mdoc/errors'; export { VerificationAssessmentId } from './mdoc/checkCallback'; export { getCborEncodeDecodeOptions, setCborEncodeDecodeOptions } from './cbor'; -export { Signer, LocalKeySigner, AzureKeyVaultSigner, AzureKeyVaultSignerConfig } from './mdoc/signing'; +export { Signer, LocalKeySigner } from './mdoc/signing'; diff --git a/src/mdoc/signing/index.ts b/src/mdoc/signing/index.ts index ec3f453..2ccfcbc 100644 --- a/src/mdoc/signing/index.ts +++ b/src/mdoc/signing/index.ts @@ -1,3 +1,2 @@ export { Signer } from './Signer'; export { LocalKeySigner } from './LocalKeySigner'; -export { AzureKeyVaultSigner, AzureKeyVaultSignerConfig } from './AzureKeyVaultSigner'; From a685a040803cfbc9dd8207c35632234a9d8feeba Mon Sep 17 00:00:00 2001 From: Warren Gallagher Date: Tue, 28 Oct 2025 13:43:57 -0400 Subject: [PATCH 3/6] feat: refine Signer abstraction so that it can infer algorithm as needed --- CONTRIBUTING.md | 2 +- .../__tests__/azureKeyVault.tests.ts | 5 ++-- .../src/AzureKeyVaultSigner.ts | 18 +++++++++++--- src/mdoc/model/Document.ts | 24 ++++++++++++++++--- src/mdoc/model/IssuerAuth.ts | 7 +++--- src/mdoc/signing/LocalKeySigner.ts | 13 +++++++--- src/mdoc/signing/Signer.ts | 13 +++++++--- 7 files changed, 64 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5485b4f..aaddcf8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ A big welcome and thank you for considering contributing to the Auth0 open sourc Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. -### Quicklinks +## Quicklinks * [Code of Conduct](#code-of-conduct) * [Getting Started](#getting-started) diff --git a/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts index 71202bc..ad18f80 100644 --- a/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts +++ b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts @@ -66,6 +66,7 @@ describeIfAzureConfigured('Azure Key Vault Signer Example', () => { signer = new AzureKeyVaultSigner({ keyVaultUrl: azureConfig.keyVaultUrl, keyName: azureConfig.keyName, + algorithm: 'ES256', // The test key is ES256 credential, }); }); @@ -88,7 +89,7 @@ describeIfAzureConfigured('Azure Key Vault Signer Example', () => { it('should sign data using Azure Key Vault', async () => { const testData = new Uint8Array([1, 2, 3, 4, 5]); - const signature = await signer.sign('ES256', testData); + const signature = await signer.sign(testData); expect(signature).toBeDefined(); expect(signature).toBeInstanceOf(Uint8Array); @@ -127,7 +128,7 @@ describeIfAzureConfigured('Azure Key Vault Signer Example', () => { .sign({ signer, // Use Azure Key Vault signer issuerCertificate: ISSUER_CERTIFICATE, - alg: 'ES256', + // alg is inferred from signer.getAlgorithm() }); const mdoc = new MDoc([document]); diff --git a/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts index a6451b0..e95e6e4 100644 --- a/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts +++ b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts @@ -3,6 +3,7 @@ import { CryptographyClient, KeyClient, SignatureAlgorithm } from '@azure/keyvau import { TokenCredential, DefaultAzureCredential } from '@azure/identity'; import { createHash } from 'crypto'; import { Signer } from '../../../src/mdoc/signing/Signer'; +import { SupportedAlgs } from '../../../src/mdoc/model/types'; export interface AzureKeyVaultSignerConfig { /** @@ -15,6 +16,11 @@ export interface AzureKeyVaultSignerConfig { */ keyName: string; + /** + * The algorithm to use for signing (must match the key type in Azure Key Vault) + */ + algorithm: SupportedAlgs; + /** * Optional key version. If not specified, uses the latest version. */ @@ -38,6 +44,7 @@ export class AzureKeyVaultSigner implements Signer { private keyVersion?: string; private credential: TokenCredential; private keyVaultUrl: string; + private algorithm: SupportedAlgs; private cachedPublicKey?: jose.JWK; constructor(config: AzureKeyVaultSignerConfig) { @@ -45,6 +52,7 @@ export class AzureKeyVaultSigner implements Signer { this.keyVaultUrl = config.keyVaultUrl.replace(/\/$/, ''); this.keyName = config.keyName; this.keyVersion = config.keyVersion; + this.algorithm = config.algorithm; this.credential = config.credential || new DefaultAzureCredential(); this.keyClient = new KeyClient(this.keyVaultUrl, this.credential); @@ -59,13 +67,13 @@ export class AzureKeyVaultSigner implements Signer { this.cryptoClient = new CryptographyClient(keyId, this.credential); } - async sign(algorithm: string, data: Uint8Array): Promise { + async sign(data: Uint8Array): Promise { // Azure Key Vault's sign operation expects a digest, not the raw data // So we need to hash the data first - const digest = this.hashData(algorithm, data); + const digest = this.hashData(this.algorithm, data); // Map COSE algorithm to Azure signature algorithm - const azureAlgorithm = this.mapCOSEtoAzureAlgorithm(algorithm); + const azureAlgorithm = this.mapCOSEtoAzureAlgorithm(this.algorithm); // Perform the signing operation in Azure Key Vault const signResult = await this.cryptoClient.sign(azureAlgorithm, digest); @@ -96,6 +104,10 @@ export class AzureKeyVaultSigner implements Signer { return this.keyName; } + getAlgorithm(): SupportedAlgs { + return this.algorithm; + } + /** * Hash the data according to the algorithm * @param algorithm - COSE algorithm identifier diff --git a/src/mdoc/model/Document.ts b/src/mdoc/model/Document.ts index 056b7c7..ae48b18 100644 --- a/src/mdoc/model/Document.ts +++ b/src/mdoc/model/Document.ts @@ -174,7 +174,7 @@ export class Document { issuerPrivateKey?: jose.JWK | Uint8Array, signer?: Signer, issuerCertificate: string | Uint8Array | Array, - alg: SupportedAlgs, + alg?: SupportedAlgs, kid?: string | Uint8Array, }): Promise { if (!this.#issuerNameSpaces) { @@ -189,6 +189,25 @@ export class Document { throw new Error('Cannot provide both issuerPrivateKey and signer. Use one or the other.'); } + // Determine the algorithm + let alg: SupportedAlgs; + if (params.signer) { + // When using signer, get algorithm from the signer + alg = params.signer.getAlgorithm(); + // If alg was also provided, validate it matches + if (params.alg && params.alg !== alg) { + throw new Error( + `Algorithm mismatch: signer uses '${alg}' but 'alg' parameter specified '${params.alg}'`, + ); + } + } else { + // When using issuerPrivateKey, alg is required + if (!params.alg) { + throw new Error('Must provide alg parameter when using issuerPrivateKey'); + } + alg = params.alg; + } + let issuerCertificateChain: Uint8Array[]; if (Array.isArray(params.issuerCertificate)) { @@ -229,7 +248,7 @@ export class Document { }; const payload = new Uint8Array(cborEncode(DataItem.fromData(mso))); - const protectedHeader: ProtectedHeaders = { alg: params.alg }; + const protectedHeader: ProtectedHeaders = { alg }; // Determine kid from params, issuerPrivateKeyJWK, or signer const { kid: paramKid, signer } = params; @@ -252,7 +271,6 @@ export class Document { unprotectedHeader, payload, signer, - params.alg, ) : await IssuerAuth.sign( protectedHeader, diff --git a/src/mdoc/model/IssuerAuth.ts b/src/mdoc/model/IssuerAuth.ts index 51380d0..f3a1354 100644 --- a/src/mdoc/model/IssuerAuth.ts +++ b/src/mdoc/model/IssuerAuth.ts @@ -172,7 +172,6 @@ export default class IssuerAuth extends Sign1 { * @param unprotectedHeaders - The unprotected headers * @param payload - The payload to sign * @param signer - The Signer implementation - * @param alg - The algorithm to use * @returns The signed IssuerAuth */ static async signWithSigner( @@ -180,8 +179,10 @@ export default class IssuerAuth extends Sign1 { unprotectedHeaders: UnprotectedHeaders | undefined, payload: Uint8Array, signer: Signer, - alg: SupportedAlgs, ): Promise { + // Get the algorithm from the signer + const alg = signer.getAlgorithm(); + // Convert headers to Maps const protectedHeadersMap = protectedHeadersToMap(protectedHeaders); @@ -211,7 +212,7 @@ export default class IssuerAuth extends Sign1 { ]); // Sign using the custom signer - const signature = await signer.sign(alg, new Uint8Array(toBeSigned)); + const signature = await signer.sign(new Uint8Array(toBeSigned)); return new IssuerAuth( new Uint8Array(encodedProtectedHeaders), diff --git a/src/mdoc/signing/LocalKeySigner.ts b/src/mdoc/signing/LocalKeySigner.ts index afa3805..c5cd32c 100644 --- a/src/mdoc/signing/LocalKeySigner.ts +++ b/src/mdoc/signing/LocalKeySigner.ts @@ -1,6 +1,7 @@ import * as jose from 'jose'; import { createSign } from 'crypto'; import { Signer } from './Signer'; +import { SupportedAlgs } from '../model/types'; /** * Local key-based signer that uses JWK for signing operations. @@ -8,17 +9,19 @@ import { Signer } from './Signer'; */ export class LocalKeySigner implements Signer { private jwk: jose.JWK; + private algorithm: SupportedAlgs; - constructor(jwk: jose.JWK) { + constructor(jwk: jose.JWK, algorithm: SupportedAlgs) { this.jwk = jwk; + this.algorithm = algorithm; } - async sign(algorithm: string, data: Uint8Array): Promise { + async sign(data: Uint8Array): Promise { // Import the JWK as a KeyLike object const key = await jose.importJWK(this.jwk); // Map COSE algorithm to Node.js digest algorithm - const digestAlgorithm = this.mapCOSEAlgorithmToNodeDigest(algorithm); + const digestAlgorithm = this.mapCOSEAlgorithmToNodeDigest(this.algorithm); // Use Node.js crypto to sign const signer = createSign(digestAlgorithm); @@ -40,6 +43,10 @@ export class LocalKeySigner implements Signer { return this.jwk.kid; } + getAlgorithm(): SupportedAlgs { + return this.algorithm; + } + /** * Map COSE algorithm names to Node.js digest algorithm names * @param coseAlg - COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') diff --git a/src/mdoc/signing/Signer.ts b/src/mdoc/signing/Signer.ts index cfc5554..cb1aa8f 100644 --- a/src/mdoc/signing/Signer.ts +++ b/src/mdoc/signing/Signer.ts @@ -1,4 +1,5 @@ import * as jose from 'jose'; +import { SupportedAlgs } from '../model/types'; /** * Interface for signing operations that can be implemented by various @@ -6,12 +7,12 @@ import * as jose from 'jose'; */ export interface Signer { /** - * Sign data using the configured signing mechanism - * @param algorithm - The COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') + * Sign data using the configured signing mechanism. + * The algorithm is determined by the signer based on its key type. * @param data - The data to sign * @returns The signature as a Uint8Array */ - sign(algorithm: string, data: Uint8Array): Promise; + sign(data: Uint8Array): Promise; /** * Get the public key information for this signer in JWK format @@ -24,4 +25,10 @@ export interface Signer { * @returns The key ID as a string, Uint8Array, or undefined */ getKeyId(): string | Uint8Array | undefined; + + /** + * Get the algorithm that this signer uses + * @returns The COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') + */ + getAlgorithm(): SupportedAlgs; } From fbaca4d6c97ce3d9311bd9050c29ebdf74e0adf9 Mon Sep 17 00:00:00 2001 From: Warren Gallagher Date: Tue, 28 Oct 2025 14:20:29 -0400 Subject: [PATCH 4/6] feat: clean up Signer abstraction further by removing unnecessary getPublicKey method --- .../__tests__/azureKeyVault.tests.ts | 11 +-- .../src/AzureKeyVaultSigner.ts | 69 ++----------------- src/mdoc/signing/LocalKeySigner.ts | 6 -- src/mdoc/signing/Signer.ts | 7 -- 4 files changed, 8 insertions(+), 85 deletions(-) diff --git a/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts index ad18f80..447352c 100644 --- a/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts +++ b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts @@ -75,18 +75,9 @@ describeIfAzureConfigured('Azure Key Vault Signer Example', () => { it('should initialize successfully', () => { expect(signer).toBeDefined(); expect(signer.getKeyId()).toBe(azureConfig!.keyName); + expect(signer.getAlgorithm()).toBe('ES256'); }); - it('should retrieve the public key from Azure Key Vault', async () => { - const publicKey = await signer.getPublicKey(); - expect(publicKey).toBeDefined(); - expect(publicKey.kty).toBe('EC'); - expect(publicKey.crv).toBe('P-256'); // ES256 uses P-256 - expect(publicKey.x).toBeDefined(); - expect(publicKey.y).toBeDefined(); - expect(publicKey.d).toBeUndefined(); // Should not have private key component - }, 10000); // 10 second timeout for network call - it('should sign data using Azure Key Vault', async () => { const testData = new Uint8Array([1, 2, 3, 4, 5]); const signature = await signer.sign(testData); diff --git a/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts index e95e6e4..30aeb78 100644 --- a/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts +++ b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts @@ -1,5 +1,4 @@ -import * as jose from 'jose'; -import { CryptographyClient, KeyClient, SignatureAlgorithm } from '@azure/keyvault-keys'; +import { CryptographyClient, SignatureAlgorithm } from '@azure/keyvault-keys'; import { TokenCredential, DefaultAzureCredential } from '@azure/identity'; import { createHash } from 'crypto'; import { Signer } from '../../../src/mdoc/signing/Signer'; @@ -39,32 +38,24 @@ export interface AzureKeyVaultSignerConfig { */ export class AzureKeyVaultSigner implements Signer { private cryptoClient: CryptographyClient; - private keyClient: KeyClient; private keyName: string; - private keyVersion?: string; - private credential: TokenCredential; - private keyVaultUrl: string; private algorithm: SupportedAlgs; - private cachedPublicKey?: jose.JWK; constructor(config: AzureKeyVaultSignerConfig) { // Normalize the key vault URL by removing trailing slash - this.keyVaultUrl = config.keyVaultUrl.replace(/\/$/, ''); + const keyVaultUrl = config.keyVaultUrl.replace(/\/$/, ''); this.keyName = config.keyName; - this.keyVersion = config.keyVersion; this.algorithm = config.algorithm; - this.credential = config.credential || new DefaultAzureCredential(); - - this.keyClient = new KeyClient(this.keyVaultUrl, this.credential); + const credential = config.credential || new DefaultAzureCredential(); // Initialize the cryptography client // If keyVersion is provided, use it; otherwise, use the key name which will use the // latest version - const keyId = this.keyVersion - ? `${this.keyVaultUrl}/keys/${this.keyName}/${this.keyVersion}` - : `${this.keyVaultUrl}/keys/${this.keyName}`; + const keyId = config.keyVersion + ? `${keyVaultUrl}/keys/${this.keyName}/${config.keyVersion}` + : `${keyVaultUrl}/keys/${this.keyName}`; - this.cryptoClient = new CryptographyClient(keyId, this.credential); + this.cryptoClient = new CryptographyClient(keyId, credential); } async sign(data: Uint8Array): Promise { @@ -81,25 +72,6 @@ export class AzureKeyVaultSigner implements Signer { return new Uint8Array(signResult.result); } - async getPublicKey(): Promise { - if (this.cachedPublicKey) { - return this.cachedPublicKey; - } - - // Retrieve the key from Azure Key Vault - const keyVaultKey = await this.keyClient.getKey(this.keyName, { version: this.keyVersion }); - - if (!keyVaultKey.key) { - throw new Error(`Failed to retrieve key ${this.keyName} from Azure Key Vault`); - } - - // Convert Azure Key Vault key to JWK format - const jwk = this.azureKeyToJWK(keyVaultKey.key); - - this.cachedPublicKey = jwk; - return jwk; - } - getKeyId(): string { return this.keyName; } @@ -192,31 +164,4 @@ export class AzureKeyVaultSigner implements Signer { return azureAlg; } - - /** - * Convert Azure Key Vault key to JWK format - * @param azureKey - Key from Azure Key Vault - * @returns JWK representation of the public key - */ - private azureKeyToJWK(azureKey: any): jose.JWK { - // Azure Key Vault returns keys in JWK format already - // We just need to extract the public key components - const jwk: jose.JWK = { - kty: azureKey.kty.replace('-HSM', ''), // Normalize EC-HSM to EC, RSA-HSM to RSA - kid: azureKey.kid, - }; - - if (azureKey.kty === 'EC' || azureKey.kty === 'EC-HSM') { - jwk.crv = azureKey.crv; - jwk.x = azureKey.x; - jwk.y = azureKey.y; - } else if (azureKey.kty === 'RSA' || azureKey.kty === 'RSA-HSM') { - jwk.n = azureKey.n; - jwk.e = azureKey.e; - } else { - throw new Error(`Unsupported key type: ${azureKey.kty}`); - } - - return jwk; - } } diff --git a/src/mdoc/signing/LocalKeySigner.ts b/src/mdoc/signing/LocalKeySigner.ts index c5cd32c..1a0beed 100644 --- a/src/mdoc/signing/LocalKeySigner.ts +++ b/src/mdoc/signing/LocalKeySigner.ts @@ -33,12 +33,6 @@ export class LocalKeySigner implements Signer { return new Uint8Array(signature); } - async getPublicKey(): Promise { - // Remove the private key component to get the public key - const { d, ...publicKey } = this.jwk; - return publicKey; - } - getKeyId(): string | Uint8Array | undefined { return this.jwk.kid; } diff --git a/src/mdoc/signing/Signer.ts b/src/mdoc/signing/Signer.ts index cb1aa8f..ce1a54a 100644 --- a/src/mdoc/signing/Signer.ts +++ b/src/mdoc/signing/Signer.ts @@ -1,4 +1,3 @@ -import * as jose from 'jose'; import { SupportedAlgs } from '../model/types'; /** @@ -14,12 +13,6 @@ export interface Signer { */ sign(data: Uint8Array): Promise; - /** - * Get the public key information for this signer in JWK format - * @returns The public key as a JWK - */ - getPublicKey(): Promise; - /** * Get the key ID for this signer * @returns The key ID as a string, Uint8Array, or undefined From 3292334be7e2aeb6a331c10272636e9d1e46670d Mon Sep 17 00:00:00 2001 From: Warren Gallagher Date: Tue, 4 Nov 2025 09:01:04 -0500 Subject: [PATCH 5/6] feat: fix documentation errors --- .../AZURE_KEY_VAULT_INTEGRATION.md | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md index 3055b08..a6e3dca 100644 --- a/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md +++ b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md @@ -24,12 +24,13 @@ Document.sign() 1. **Signer Interface** (`src/mdoc/signing/Signer.ts`) - Abstract interface for signing operations - - Supports `sign()`, `getPublicKey()`, and `getKeyId()` methods + - Supports `sign()`, `getKeyId()`, and `getAlgorithm()` methods -2. **AzureKeyVaultSigner** (`src/mdoc/signing/AzureKeyVaultSigner.ts`) - - Implements Signer interface using Azure Key Vault +2. **AzureKeyVaultSigner** (`examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts`) + - Example implementation of Signer interface using Azure Key Vault - Uses `@azure/keyvault-keys` and `@azure/identity` SDKs - Automatically hashes data and maps COSE algorithms to Azure signature algorithms + - Note: This is an example implementation, not part of the core library 3. **LocalKeySigner** (`src/mdoc/signing/LocalKeySigner.ts`) - Implements Signer interface for local JWK keys @@ -47,13 +48,17 @@ Document.sign() ## Changes Made -### New Files +### New Files in Core Library - `src/mdoc/signing/Signer.ts` - Signer interface - `src/mdoc/signing/LocalKeySigner.ts` - Local key implementation -- `src/mdoc/signing/AzureKeyVaultSigner.ts` - Azure Key Vault implementation - `src/mdoc/signing/index.ts` - Module exports -- `AZURE_KEY_VAULT_EXAMPLE.md` - Usage examples and documentation + +### Example Implementation + +- `examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts` - Azure Key Vault example implementation +- `examples/azure-keyvault-signer/AZURE_KEY_VAULT_EXAMPLE.md` - Usage examples and documentation +- `examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md` - Integration guide (this document) ### Modified Files @@ -67,22 +72,27 @@ Document.sign() - Integrated custom signer path - `src/index.ts` - - Exported new signing classes and interfaces + - Exported `Signer` interface and `LocalKeySigner` class + - Note: `AzureKeyVaultSigner` is not exported from the main library; it's an example implementation -- `package.json` - - Added `@azure/identity` (^4.0.0) - - Added `@azure/keyvault-keys` (^4.8.0) +- `examples/azure-keyvault-signer/package.json` + - Lists `@azure/identity` (^4.0.0) as peer dependency + - Lists `@azure/keyvault-keys` (^4.8.0) as peer dependency + - Note: These Azure packages are NOT dependencies of the core `@auth0/mdl` library + - Users must install these packages separately if using the Azure Key Vault example ## Usage ### Quick Start ```typescript -import { Document, AzureKeyVaultSigner } from '@auth0/mdl'; +import { Document } from '@auth0/mdl'; +import { AzureKeyVaultSigner } from './examples/azure-keyvault-signer/src/AzureKeyVaultSigner'; const signer = new AzureKeyVaultSigner({ keyVaultUrl: 'https://my-vault.vault.azure.net', keyName: 'my-signing-key', + algorithm: 'ES256', // Required: must match your key type }); const doc = new Document('org.iso.18013.5.1.mDL'); @@ -91,7 +101,6 @@ const doc = new Document('org.iso.18013.5.1.mDL'); const signedDoc = await doc.sign({ signer, issuerCertificate: certificatePEM, - alg: 'ES256', }); ``` From 9e8770cf3560df0ab8df1736be222ed0bd781ffb Mon Sep 17 00:00:00 2001 From: Warren Gallagher Date: Tue, 4 Nov 2025 14:50:41 -0500 Subject: [PATCH 6/6] feat: fix LocalKeySigner.ts and add signerCompatibility.tests.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LocalKeySigner had two critical bugs that prevented signatures from being verified: Double-hashing: Used createSign() which hashes data, but the data was already a Sig_structure Wrong signature format: Used DER encoding instead of IEEE P1363 format required by COSE Solution Implemented Fixed src/mdoc/signing/LocalKeySigner.ts: Changes Made: Replaced createSign() with crypto.sign() - The one-shot API that matches cose-kit's behavior Added dsaEncoding: 'ieee-p1363' - Specifies COSE-compliant signature format (raw R||S concatenation) Simplified digest mapping - RSA algorithms now correctly map to digest names only (e.g., 'sha256' not 'RSA-SHA256') Test Results ✅ All tests pass! The new tests/issuing/signerCompatibility.tests.ts confirms: ✅ Documents are verifiable - Both old (issuerPrivateKey) and new (LocalKeySigner) methods produce valid, verifiable signatures ✅ Identical structure - Documents have the same COSE structure, algorithm identifiers, and payload ✅ Backward compatibility - Old signing method still works perfectly ✅ Proper validation - Correctly rejects invalid parameter combinations Technical Details The fix ensures LocalKeySigner follows the same signing process as cose-kit: Sig_structure → hash with SHA-256/384/512 → sign with private key → IEEE P1363 format Instead of the broken: Sig_structure → hash → hash again → sign → DER format ❌ The implementation now correctly uses: cryptoSign(digestAlgorithm, data, { key: key, dsaEncoding: 'ieee-p1363', // COSE format! }); --- .../issuing/signerCompatibility.tests.ts | 276 ++++++++++++++++++ src/mdoc/signing/LocalKeySigner.ts | 37 +-- 2 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 __tests__/issuing/signerCompatibility.tests.ts diff --git a/__tests__/issuing/signerCompatibility.tests.ts b/__tests__/issuing/signerCompatibility.tests.ts new file mode 100644 index 0000000..dc92938 --- /dev/null +++ b/__tests__/issuing/signerCompatibility.tests.ts @@ -0,0 +1,276 @@ +import * as jose from 'jose'; +import { + Document, + LocalKeySigner, + MDoc, + parse, + Verifier, +} from '../../src'; +import { DEVICE_JWK, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK } from './config'; + +const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; + +describe('Signer Compatibility Tests', () => { + const signed = new Date('2023-10-24T14:55:18Z'); + const validFrom = new Date(signed); + validFrom.setMinutes(signed.getMinutes() + 5); + const validUntil = new Date(signed); + validUntil.setFullYear(signed.getFullYear() + 30); + const expectedUpdate = new Date(signed); + expectedUpdate.setFullYear(signed.getFullYear() + 1); + + const sharedDocumentData = { + family_name: 'Jones', + given_name: 'Ava', + birth_date: '2007-03-25', + issue_date: '2023-09-01', + expiry_date: '2028-09-30', + issuing_country: 'US', + issuing_authority: 'NY DMV', + document_number: '01-856-5050', + portrait: 'bstr', + driving_privileges: [ + { + vehicle_category_code: 'A', + issue_date: '2021-09-02', + expiry_date: '2026-09-20', + }, + { + vehicle_category_code: 'B', + issue_date: '2022-09-02', + expiry_date: '2027-09-20', + }, + ], + }; + + describe('Document.sign() - Old vs New Method', () => { + let encodedWithOldMethod: Uint8Array; + let encodedWithNewMethod: Uint8Array; + + beforeAll(async () => { + // Sign with old method using issuerPrivateKey + const docOld = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', sharedDocumentData) + .useDigestAlgorithm('SHA-256') + .addValidityInfo({ + signed, + validFrom, + validUntil, + expectedUpdate, + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + issuerPrivateKey: ISSUER_PRIVATE_KEY_JWK, + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + }); + + const mdocOld = new MDoc([docOld]); + encodedWithOldMethod = mdocOld.encode(); + + // Sign with new method using LocalKeySigner + const signer = new LocalKeySigner(ISSUER_PRIVATE_KEY_JWK, 'ES256'); + const docNew = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', sharedDocumentData) + .useDigestAlgorithm('SHA-256') + .addValidityInfo({ + signed, + validFrom, + validUntil, + expectedUpdate, + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + signer, + issuerCertificate: ISSUER_CERTIFICATE, + }); + + const mdocNew = new MDoc([docNew]); + encodedWithNewMethod = mdocNew.encode(); + }); + + it('should produce valid documents with both methods', () => { + expect(encodedWithOldMethod).toBeDefined(); + expect(encodedWithOldMethod).toBeInstanceOf(Uint8Array); + expect(encodedWithOldMethod.length).toBeGreaterThan(0); + + expect(encodedWithNewMethod).toBeDefined(); + expect(encodedWithNewMethod).toBeInstanceOf(Uint8Array); + expect(encodedWithNewMethod.length).toBeGreaterThan(0); + }); + + it('should both be verifiable by the Verifier', async () => { + const verifier = new Verifier([ISSUER_CERTIFICATE]); + + // Verify old method document + await expect( + verifier.verify(encodedWithOldMethod, { + onCheck: (verification, original) => { + if (verification.category === 'DEVICE_AUTH') { + return; + } + original(verification); + }, + }), + ).resolves.not.toThrow(); + + // Verify new method document + await expect( + verifier.verify(encodedWithNewMethod, { + onCheck: (verification, original) => { + if (verification.category === 'DEVICE_AUTH') { + return; + } + original(verification); + }, + }), + ).resolves.not.toThrow(); + }); + + it('should produce documents with identical structure', () => { + const parsedOld = parse(encodedWithOldMethod); + const parsedNew = parse(encodedWithNewMethod); + + const docOld = parsedOld.documents[0]; + const docNew = parsedNew.documents[0]; + + // Compare document type + expect(docOld.docType).toEqual(docNew.docType); + + // Compare validity info + const validityInfoOld = docOld.issuerSigned.issuerAuth.decodedPayload.validityInfo; + const validityInfoNew = docNew.issuerSigned.issuerAuth.decodedPayload.validityInfo; + expect(validityInfoOld.signed).toEqual(validityInfoNew.signed); + expect(validityInfoOld.validFrom).toEqual(validityInfoNew.validFrom); + expect(validityInfoOld.validUntil).toEqual(validityInfoNew.validUntil); + expect(validityInfoOld.expectedUpdate).toEqual(validityInfoNew.expectedUpdate); + + // Compare digest algorithm + expect(docOld.issuerSigned.issuerAuth.decodedPayload.digestAlgorithm) + .toEqual(docNew.issuerSigned.issuerAuth.decodedPayload.digestAlgorithm); + + // Compare namespace data + const attrValuesOld = docOld.getIssuerNameSpace('org.iso.18013.5.1'); + const attrValuesNew = docNew.getIssuerNameSpace('org.iso.18013.5.1'); + expect(attrValuesOld).toEqual(attrValuesNew); + }); + + it('should have the same algorithm identifier', () => { + const parsedOld = parse(encodedWithOldMethod); + const parsedNew = parse(encodedWithNewMethod); + + const docOld = parsedOld.documents[0]; + const docNew = parsedNew.documents[0]; + + // Both documents should use ES256 (-7) + expect(docOld.issuerSigned.issuerAuth.alg).toEqual(docNew.issuerSigned.issuerAuth.alg); + expect(docOld.issuerSigned.issuerAuth.alg).toEqual(-7); // -7 is the COSE algorithm identifier for ES256 + }); + + it('should have valid COSE signatures with both methods', () => { + const parsedOld = parse(encodedWithOldMethod); + const parsedNew = parse(encodedWithNewMethod); + + const docOld = parsedOld.documents[0]; + const docNew = parsedNew.documents[0]; + + // Both should have valid signatures + expect(docOld.issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); + expect(docOld.issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); + + expect(docNew.issuerSigned.issuerAuth.signature).toBeInstanceOf(Uint8Array); + expect(docNew.issuerSigned.issuerAuth.signature.length).toBeGreaterThan(0); + + // Signatures will be different due to randomness in ECDSA signing, + // but both should be valid (verified by the Verifier test above) + }); + }); + + describe('LocalKeySigner implementation', () => { + it('should correctly implement the Signer interface', () => { + const signer = new LocalKeySigner(ISSUER_PRIVATE_KEY_JWK, 'ES256'); + + expect(signer.getKeyId()).toBe(ISSUER_PRIVATE_KEY_JWK.kid); + expect(signer.getAlgorithm()).toBe('ES256'); + }); + + it('should be able to sign data', async () => { + const signer = new LocalKeySigner(ISSUER_PRIVATE_KEY_JWK, 'ES256'); + const testData = new Uint8Array([1, 2, 3, 4, 5]); + const signature = await signer.sign(testData); + + expect(signature).toBeDefined(); + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBeGreaterThan(0); + }); + }); + + describe('Backward compatibility', () => { + it('should still support the old issuerPrivateKey method', async () => { + const doc = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Test', + given_name: 'User', + birth_date: '1990-01-01', + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + issuerPrivateKey: ISSUER_PRIVATE_KEY_JWK, + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + }); + + const mdoc = new MDoc([doc]); + const encoded = mdoc.encode(); + const verifier = new Verifier([ISSUER_CERTIFICATE]); + + await expect( + verifier.verify(encoded, { + onCheck: (verification, original) => { + if (verification.category === 'DEVICE_AUTH') { + return; + } + original(verification); + }, + }), + ).resolves.not.toThrow(); + }); + + it('should reject when both issuerPrivateKey and signer are provided', async () => { + const signer = new LocalKeySigner(ISSUER_PRIVATE_KEY_JWK, 'ES256'); + const doc = new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Test', + given_name: 'User', + birth_date: '1990-01-01', + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }); + + await expect( + doc.sign({ + issuerPrivateKey: ISSUER_PRIVATE_KEY_JWK, // Old method + signer, // New method + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + } as any), + ).rejects.toThrow('Cannot provide both issuerPrivateKey and signer'); + }); + + it('should reject when neither issuerPrivateKey nor signer is provided', async () => { + const doc = new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Test', + given_name: 'User', + birth_date: '1990-01-01', + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }); + + await expect( + doc.sign({ + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + } as any), + ).rejects.toThrow('Must provide either issuerPrivateKey or signer'); + }); + }); +}); diff --git a/src/mdoc/signing/LocalKeySigner.ts b/src/mdoc/signing/LocalKeySigner.ts index 1a0beed..b8dda58 100644 --- a/src/mdoc/signing/LocalKeySigner.ts +++ b/src/mdoc/signing/LocalKeySigner.ts @@ -1,5 +1,5 @@ import * as jose from 'jose'; -import { createSign } from 'crypto'; +import { sign as cryptoSign } from 'crypto'; import { Signer } from './Signer'; import { SupportedAlgs } from '../model/types'; @@ -17,19 +17,22 @@ export class LocalKeySigner implements Signer { } async sign(data: Uint8Array): Promise { - // Import the JWK as a KeyLike object + // Import the JWK as a KeyObject const key = await jose.importJWK(this.jwk); // Map COSE algorithm to Node.js digest algorithm - const digestAlgorithm = this.mapCOSEAlgorithmToNodeDigest(this.algorithm); + const digestAlgorithm = this.mapCOSEAlgorithmToDigest(this.algorithm); - // Use Node.js crypto to sign - const signer = createSign(digestAlgorithm); - signer.update(data); - signer.end(); - - // KeyLike from jose.importJWK is compatible with Node.js crypto - const signature = signer.sign(key as any); + // Use Node.js crypto.sign() one-shot API + // This API will hash the data once with the specified digest algorithm + // and then sign the hash, which is what COSE expects + // + // IMPORTANT: COSE uses IEEE P1363 format (raw R||S concatenation) for ECDSA signatures, + // not DER encoding. We must specify dsaEncoding: 'ieee-p1363' for EC algorithms. + const signature = cryptoSign(digestAlgorithm, data, { + key: key as any, + dsaEncoding: 'ieee-p1363', + }); return new Uint8Array(signature); } @@ -46,17 +49,17 @@ export class LocalKeySigner implements Signer { * @param coseAlg - COSE algorithm identifier (e.g., 'ES256', 'ES384', 'ES512') * @returns Node.js digest algorithm name */ - private mapCOSEAlgorithmToNodeDigest(coseAlg: string): string { + private mapCOSEAlgorithmToDigest(coseAlg: string): string { const algorithmMap: Record = { ES256: 'sha256', ES384: 'sha384', ES512: 'sha512', - RS256: 'RSA-SHA256', - RS384: 'RSA-SHA384', - RS512: 'RSA-SHA512', - PS256: 'RSA-SHA256', - PS384: 'RSA-SHA384', - PS512: 'RSA-SHA512', + RS256: 'sha256', + RS384: 'sha384', + RS512: 'sha512', + PS256: 'sha256', + PS384: 'sha384', + PS512: 'sha512', }; const nodeAlg = algorithmMap[coseAlg];