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/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/__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/examples/azure-keyvault-signer/AZURE_KEY_VAULT_EXAMPLE.md b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_EXAMPLE.md new file mode 100644 index 0000000..5bd928e --- /dev/null +++ b/examples/azure-keyvault-signer/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/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md new file mode 100644 index 0000000..a6e3dca --- /dev/null +++ b/examples/azure-keyvault-signer/AZURE_KEY_VAULT_INTEGRATION.md @@ -0,0 +1,213 @@ +# 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()`, `getKeyId()`, and `getAlgorithm()` methods + +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 + - 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 in Core Library + +- `src/mdoc/signing/Signer.ts` - Signer interface +- `src/mdoc/signing/LocalKeySigner.ts` - Local key implementation +- `src/mdoc/signing/index.ts` - Module exports + +### 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 + +- `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 `Signer` interface and `LocalKeySigner` class + - Note: `AzureKeyVaultSigner` is not exported from the main library; it's an example implementation + +- `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 } 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'); +// ... add namespaces ... + +const signedDoc = await doc.sign({ + signer, + issuerCertificate: certificatePEM, +}); +``` + +### 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/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..447352c --- /dev/null +++ b/examples/azure-keyvault-signer/__tests__/azureKeyVault.tests.ts @@ -0,0 +1,197 @@ +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, + algorithm: 'ES256', // The test key is ES256 + credential, + }); + }); + + describe('AzureKeyVaultSigner', () => { + it('should initialize successfully', () => { + expect(signer).toBeDefined(); + expect(signer.getKeyId()).toBe(azureConfig!.keyName); + expect(signer.getAlgorithm()).toBe('ES256'); + }); + + it('should sign data using Azure Key Vault', async () => { + 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); + // 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 is inferred from signer.getAlgorithm() + }); + + 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/examples/azure-keyvault-signer/sample-azure-env.sh b/examples/azure-keyvault-signer/sample-azure-env.sh new file mode 100755 index 0000000..6ed33cd --- /dev/null +++ b/examples/azure-keyvault-signer/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/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts new file mode 100644 index 0000000..30aeb78 --- /dev/null +++ b/examples/azure-keyvault-signer/src/AzureKeyVaultSigner.ts @@ -0,0 +1,167 @@ +import { CryptographyClient, SignatureAlgorithm } from '@azure/keyvault-keys'; +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 { + /** + * 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; + + /** + * 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. + */ + 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 keyName: string; + private algorithm: SupportedAlgs; + + constructor(config: AzureKeyVaultSignerConfig) { + // Normalize the key vault URL by removing trailing slash + const keyVaultUrl = config.keyVaultUrl.replace(/\/$/, ''); + this.keyName = config.keyName; + this.algorithm = config.algorithm; + 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 = config.keyVersion + ? `${keyVaultUrl}/keys/${this.keyName}/${config.keyVersion}` + : `${keyVaultUrl}/keys/${this.keyName}`; + + this.cryptoClient = new CryptographyClient(keyId, credential); + } + + 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(this.algorithm, data); + + // Map COSE algorithm to Azure signature algorithm + const azureAlgorithm = this.mapCOSEtoAzureAlgorithm(this.algorithm); + + // Perform the signing operation in Azure Key Vault + const signResult = await this.cryptoClient.sign(azureAlgorithm, digest); + + return new Uint8Array(signResult.result); + } + + getKeyId(): string { + return this.keyName; + } + + getAlgorithm(): SupportedAlgs { + return this.algorithm; + } + + /** + * 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; + } +} 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/src/index.ts b/src/index.ts index 25ef185..bf464b4 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 } from './mdoc/signing'; diff --git a/src/mdoc/model/Document.ts b/src/mdoc/model/Document.ts index 4ad7d3d..ae48b18 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,22 +163,51 @@ 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, + alg?: SupportedAlgs, kid?: string | Uint8Array, }): Promise { if (!this.#issuerNameSpaces) { 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.'); + } + + // 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)) { @@ -188,11 +218,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 +247,37 @@ export class Document { validityInfo: this.#validityInfo, }; - const payload = cborEncode(DataItem.fromData(mso)); - const protectedHeader: ProtectedHeaders = { alg: params.alg }; + const payload = new Uint8Array(cborEncode(DataItem.fromData(mso))); + const protectedHeader: ProtectedHeaders = { 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, + ) : + 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..f3a1354 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,62 @@ 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 + * @returns The signed IssuerAuth + */ + static async signWithSigner( + protectedHeaders: ProtectedHeaders, + unprotectedHeaders: UnprotectedHeaders | undefined, + payload: Uint8Array, + signer: Signer, + ): Promise { + // Get the algorithm from the signer + const alg = signer.getAlgorithm(); + + // 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(new Uint8Array(toBeSigned)); + + return new IssuerAuth( + new Uint8Array(encodedProtectedHeaders), + unprotectedHeadersMap, + payload, + signature, + ); + } } diff --git a/src/mdoc/signing/LocalKeySigner.ts b/src/mdoc/signing/LocalKeySigner.ts new file mode 100644 index 0000000..b8dda58 --- /dev/null +++ b/src/mdoc/signing/LocalKeySigner.ts @@ -0,0 +1,72 @@ +import * as jose from 'jose'; +import { sign as cryptoSign } from 'crypto'; +import { Signer } from './Signer'; +import { SupportedAlgs } from '../model/types'; + +/** + * 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; + private algorithm: SupportedAlgs; + + constructor(jwk: jose.JWK, algorithm: SupportedAlgs) { + this.jwk = jwk; + this.algorithm = algorithm; + } + + async sign(data: Uint8Array): Promise { + // Import the JWK as a KeyObject + const key = await jose.importJWK(this.jwk); + + // Map COSE algorithm to Node.js digest algorithm + const digestAlgorithm = this.mapCOSEAlgorithmToDigest(this.algorithm); + + // 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); + } + + getKeyId(): string | Uint8Array | undefined { + 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') + * @returns Node.js digest algorithm name + */ + private mapCOSEAlgorithmToDigest(coseAlg: string): string { + const algorithmMap: Record = { + ES256: 'sha256', + ES384: 'sha384', + ES512: 'sha512', + RS256: 'sha256', + RS384: 'sha384', + RS512: 'sha512', + PS256: 'sha256', + PS384: 'sha384', + PS512: '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..ce1a54a --- /dev/null +++ b/src/mdoc/signing/Signer.ts @@ -0,0 +1,27 @@ +import { SupportedAlgs } from '../model/types'; + +/** + * 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. + * 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(data: Uint8Array): Promise; + + /** + * Get the key ID for this 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; +} diff --git a/src/mdoc/signing/index.ts b/src/mdoc/signing/index.ts new file mode 100644 index 0000000..2ccfcbc --- /dev/null +++ b/src/mdoc/signing/index.ts @@ -0,0 +1,2 @@ +export { Signer } from './Signer'; +export { LocalKeySigner } from './LocalKeySigner';