> Markdown version of https://docs.laavat.io/exampleflows/devicecertificate/ from the LAAVAT PKI and Signing Platform documentation. All pages: https://docs.laavat.io/llms.txt

# Device certificate example

> **Tip: Looking for the full device identity design?**
> This page walks through issuing a device certificate from one CA. For the
> complete IEEE 802.1AR design — an IDevID at manufacturing over REST and an
> LDevID in the field over EST, with the manufacturer and operator PKIs
> behind them — see
> [Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/).

This example shows how to create a product which creates an internal Certificate Authority for issuing Initial Device certificates.

This example uses the reference python client and the UI to perform different actions.

The example is divided to following stages:

- How to get authentication token
- Adding certificate profiles
- Creating product
- Issuing Initial Device certificates

## Authentication

In order to use the reference python client a valid JWT token is needed. How to obtain one is explained in more detail in the [authentication chapter.](https://docs.laavat.io/api/authentication/)
New token must be requested if the current token expires. ( roughly 1 hour of validity)

```bash
(venv) $ az login --allow-no-subscriptions --only-show-error
A web browser has been opened at https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.

[1]    N/A(tenant level account)  <redacted>  <redacted>

The default is marked with an *; the default tenant is '<redacted>' and subscription is 'N/A(tenant level account)' (<redacted>).

Select a subscription and tenant (Type a number or Enter for no changes): 1

Tenant: <redacted>
Subscription: N/A(tenant level account) (<redacted>)


(venv) $ az account get-access-token --resource api://<redacted>
{
  "accessToken": "<redacted>",
  "expiresOn": "2025-06-04 15:18:10.000000",
  "expires_on": 1749039490,
  "subscription": "<redacted>",
  "tenant": "<redacted>",
  "tokenType": "Bearer"
}
(venv) $ export TOKEN=<redacted>
```

## Add profiles

> **Note: Token handling in these examples**
> The `signing-tool` commands below use `-n test.ini` — a config file created with `config-init` that references
> a `token_file` rather than embedding a token, so no token appears on the command line. Approver steps use `-n
> approver.ini` (the same config for a user in the approvers group). See [Reference Client ›
> Usage](https://docs.laavat.io/democlient/usage/) for how to create these configs and for the secure token-input forms (`-t
> @file`, `-t @-`).

In this example a Certificate Authority is created and two profiles are needed. One for the Certificate Authority and one for the issued End Entities.

The [example profiles](https://docs.laavat.io/usage/productguide/pki-examples/#example-of-device-certificate-profiles) were taken as base and modified for this example

```bash
(venv) $ signing-tool -n test.ini profile \
    add -F rootca.yaml -N "Device CA root" -T ROOT
{
    "id": "a80ce30e-d992-4907-90b8-b3da744c89ca",
    "profile_name": "Device CA root",
    "profile_type": 3,
    "profile_yaml": "IyBDb25maWcg…(3,664 base64 chars elided; text, shown decoded below as value 1 of 2)"
}
Profile added. Profile ID: a80ce30e-d992-4907-90b8-b3da744c89ca

(venv) $ signing-tool -n test.ini profile \
    add -F endentity.yaml -N "Device CA endentity" -T END
{
    "id": "f1d0e083-1cf1-41da-8284-545f57550764",
    "profile_name": "Device CA endentity",
    "profile_type": 1,
    "profile_yaml": "IyBDb25maWcg…(3,244 base64 chars elided; text, shown decoded below as value 2 of 2)"
}
Profile added. Profile ID: f1d0e083-1cf1-41da-8284-545f57550764

(venv) $ signing-tool -n test.ini profile getall
{
    "count": 2,
    "items": [
        {
            "id": "f1d0e083-1cf1-41da-8284-545f57550764",
            "profile_name": "Device CA endentity",
            "profile_type": 1
        },
        {
            "id": "a80ce30e-d992-4907-90b8-b3da744c89ca",
            "profile_name": "Device CA root",
            "profile_type": 3
        }
    ],
    "next": "/cas/profiles/?page=1",
    "pages": 1,
    "prev": "/cas/profiles/?page=1"
}
```

Decoded base64 value 1 of 2:

```text
# Config file for the SubCA profile. Do not use TABS
---
Name: Initial Device Certificate Product Family SubCA Profile
# Distinguished name of the CA
DN:
  # Organization: 
  Organization: Laavat
  # OrganizationalUnit: R&D
  # Country: FI
  Country: FI
  # Province:
  # Locality:
  # StreetAddress:
  # PostalCode:
  # SerialNumber
  # CommonName: IMX6
# EndEntity is 1, Sub CA profile 2 , Root profile 3
ProfileType: 3
# SignatureAlgorithms
#  SHA1WithRSA = 3
#  SHA256WithRSA = 4
#  SHA384WithRSA = 5
#  SHA512WithRSA = 6
#  DSAWithSHA1 = 7
#  DSAWithSHA256 = 8
#  ECDSAWithSHA1 = 9
#  ECDSAWithSHA256 = 10
#  ECDSAWithSHA384 = 11
#  ECDSAWithSHA512 = 12
#  SHA256WithRSAPSS = 13
#  SHA384WithRSAPSS = 14
#  SHA512WithRSAPSS = 15
SignatureAlgorithm: 10
# KeyAlgorithms
#   RSAAlgorithm = 1
#   ECDSAAlgorithm = 2
KeyAlgorithm: 2
# Validity of the certificate in duration (max 290 years) or with absolute date in RFC3339 e.g. 9999-12-31T23:59:59Z.
# Example in absolute date "9999-12-31T23:59:59Z".
# Example in duration "1h10m10s". Only h, m and/or s are accepted.
ValidityPeriod: "9999-12-31T23:59:59Z"
BasicConstraints:
  BasicConstraintsValid: true
  IsCA: true
  MaxPathLen: 0
  # MaxPathLenZero indicates that BasicConstraintsValid==true
  # and MaxPathLen==0 should be interpreted as an actual
  # maximum path length of zero. Otherwise, that combination is
  # interpreted as MaxPathLen not being set.
  MaxPathLenZero: true
AddSKI: true
AddAKI: true
# KeyUsages
#  KeyUsageDigitalSignature = 1
#  KeyUsageContentCommitment = 2
#  KeyUsageKeyEncipherment = 4
#  KeyUsageDataEncipherment = 8
#  KeyUsageKeyAgreement = 16
#  KeyUsageCertSign = 32 
#  KeyUsageCRLSign = 64
#  KeyUsageEncipherOnly = 128
#  KeyUsageDecipherOnly = 256
KeyUsage:
  - 1
  - 32
  - 64
ExtendedKeyUsage:
  # ExtendedKeyUsages
  #  ExtKeyUsageAny = 0
  #  ExtKeyUsageServerAuth = 1
  #  ExtKeyUsageClientAuth = 2
  #  ExtKeyUsageCodeSigning = 3
  #  ExtKeyUsageEmailProtection = 4
  #  ExtKeyUsageIPSECEndSystem = 5
  #  ExtKeyUsageIPSECTunnel = 6
  #  ExtKeyUsageIPSECUser = 7
  #  ExtKeyUsageTimeStamping = 8
  #  ExtKeyUsageOCSPSigning = 9
  #  ExtKeyUsageMicrosoftServerGatedCrypto = 10
  #  ExtKeyUsageNetscapeServerGatedCrypto = 11
  #  ExtKeyUsageMicrosoftCommercialCodeSigning = 12
  #  ExtKeyUsageMicrosoftKernelCodeSigning = 13
# ExtraExtensions: true
# SANUsage: true
# CRLDistributionPoints. List of URI strings.
#CRLDistributionPoints:
#  - http://crl.laavat.com/products-0/root.crl

# PolicyIdentifiers. List of ASN1 policy OIDS
#PolicyIdentifiers:
#  - 1.10.123.432.4.5
#  - 2.10.123.432.4.65

# EnforceUniqueDN enables the checking if a certificate has been issued with the same subject DN from the CA
# Values true or false
#EnforceUniqueDN: true
```

Decoded base64 value 2 of 2:

```text
# Config file for the End entity profile. Do not use TABS
---
Name: Initial Device Certificate EndEntity Profile
DN:
  #Organization: Test Comp
  Organization: Laavat
  #OrganizationalUnit: R&D
  #Country: FI
  Country: FI
  #Province:
  #- Something
  # Locality:
  # StreetAddress:
  # PostalCode:
  # SerialNumber
  # CommonName: 
# EndEntity is 1, Sub CA profile 2 , Root profile 3
ProfileType: 1
# SignatureAlgorithms
#  SHA1WithRSA = 3
#  SHA256WithRSA = 4
#  SHA384WithRSA = 5
#  SHA512WithRSA = 6
#  DSAWithSHA1 = 7
#  DSAWithSHA256 = 8
#  ECDSAWithSHA1 = 9
#  ECDSAWithSHA256 = 10
#  ECDSAWithSHA384 = 11
#  ECDSAWithSHA512 = 12
#  SHA256WithRSAPSS = 13
#  SHA384WithRSAPSS = 14
#  SHA512WithRSAPSS = 15
SignatureAlgorithm: 10
# KeyAlgorithms
#   RSAAlgorithm = 1
#   ECDSAAlgorithm = 2
KeyAlgorithm: 2
# Validity of the certificate in duration (max 290 years) or with absolute date in RFC3339 e.g. 9999-12-31T23:59:59Z.
# Example in absolute date "9999-12-31T23:59:59Z".
# Example in duration "1h10m10s". Only h, m and/or s are accepted.
ValidityPeriod: "9999-12-31T23:59:59Z"
BasicConstraints:
  Use: true
  IsCA: false
AddSKI: true
AddAKI: true
# KeyUsages
#  KeyUsageDigitalSignature = 1
#  KeyUsageContentCommitment = 2
#  KeyUsageKeyEncipherment = 4
#  KeyUsageDataEncipherment = 8
#  KeyUsageKeyAgreement = 16
#  KeyUsageCertSign = 32 
#  KeyUsageCRLSign = 64
#  KeyUsageEncipherOnly = 128
#  KeyUsageDecipherOnly = 256
KeyUsage:
  - 1
  - 16
ExtendedKeyUsage:
  # ExtendedKeyUsages
  #  ExtKeyUsageAny = 0
  #  ExtKeyUsageServerAuth = 1
  #  ExtKeyUsageClientAuth = 2
  #  ExtKeyUsageCodeSigning = 3
  #  ExtKeyUsageEmailProtection = 4
  #  ExtKeyUsageIPSECEndSystem = 5
  #  ExtKeyUsageIPSECTunnel = 6
  #  ExtKeyUsageIPSECUser = 7
  #  ExtKeyUsageTimeStamping = 8
  #  ExtKeyUsageOCSPSigning = 9
  #  ExtKeyUsageMicrosoftServerGatedCrypto = 10
  #  ExtKeyUsageNetscapeServerGatedCrypto = 11
  #  ExtKeyUsageMicrosoftCommercialCodeSigning = 12
  #  ExtKeyUsageMicrosoftKernelCodeSigning = 13
  - 1
  - 2
ExtraExtensions: true
SANUsage: true
# CRLDistributionPoints. List of URI strings.
CRLDistributionPoints:
  - http://crl.laavat.com/e37f045c.crl
# PolicyIdentifiers. List of ASN1 policy OIDS
PolicyIdentifiers:
  - 1.10.123.432.4.5
  - 2.10.123.432.4.65
# EnforceUniqueDN enables the checking if a certificate has been issued with the same subject DN from the CA
# Values true or false
# EnforceUniqueDN: true
```

## Create product

Here is the used [product template](https://docs.laavat.io/usage/productguide/product/#device-certificate-issuance). 
The product template was updated with the profile ids obtained from the previous commands. The values $ROOTPROFILEID, $ENDPROFILEID were changed with their corresponding profile id.

- ROOTPROFILEID=a80ce30e-d992-4907-90b8-b3da744c89ca
- ENDPROFILEID=f1d0e083-1cf1-41da-8284-545f57550764

In this product there is one operation.
In the approval rule it was decided that entities in the $WRITERGROUP were able to make issuance  requests. 

$WRITERGROUP is replaced with the corresponding Group Object ID from Microsoft Entra.

The template was saved as product.json.

More information about rules can be found from [approval rules.](https://docs.laavat.io/usage/productguide/product/#approval-rules)

Product was created using the python tool

```bash
(venv) $ signing-tool -n test.ini product add -T product.json 
{
    "ca_info": [
        {
            "cert_override_payload": null,
            "certificate_type": "ROOT",
            "cn": "Dummy CA",
            "crl_distribution_points": null,
            "crl_expiry": null,
            "crl_issue_interval": null,
            "csr": null,
            "description": "Device Identity CA for Dummy Device CA issuer",
            "external_request_id": null,
            "external_root": null,
            "id": null,
            "issuer_ca_id": null,
            "key_override_id": null,
            "key_type": "ECDSAP256",
            "leafs": [],
            "originating_id": null,
            "policy_identifiers": null,
            "product_id": null,
            "profile_id": "a80ce30e-d992-4907-90b8-b3da744c89ca",
            "state": null,
            "use_case": "DeviceCA"
        }
    ],
    "description": "Device certificate for Initial Device certificates",
    "enabled": true,
    "external_request_id": null,
    "id": null,
    "name": "Dummy Device CA issuer",
    "product_config_items": null,
    "product_operations": [
        {
            "approval_rule": {
                "allowed_groups": [
                    "b716abb1-2e3b-47a9-bca5-a3669faa50a6"
                ],
                "approval_groups": [],
                "blanket_groups": [],
                "description": "desc",
                "name": "This is rule"
            },
            "ca_use_case": null,
            "description": "Device certificates",
            "id": null,
            "name": "Issue device certificates",
            "operation_type": "IssueDeviceCertificate",
            "profile_id": "f1d0e083-1cf1-41da-8284-545f57550764",
            "token": null
        }
    ],
    "product_type": "Production",
    "rnd_keys": [],
    "state": null
}
Is product ok(Y/N): y
{
    "ca_info": [],
    "description": "Device certificate for Initial Device certificates",
    "enabled": null,
    "external_request_id": null,
    "id": "c57c5925-9a5c-4c57-a70a-74e68a8ddc4f",
    "name": "Dummy Device CA issuer",
    "product_config_items": null,
    "product_operations": [],
    "product_type": null,
    "rnd_keys": [],
    "state": 2
}
Product Add request sent. Request ID: c57c5925-9a5c-4c57-a70a-74e68a8ddc4f state: ApprovalRequired
```

## Approve product

The product was approved using the GUI.

Then with the python tool the state was checked ( state is 16 for ready product) and the product ID and operation ID was obtained.

- productID = c57c5925-9a5c-4c57-a70a-74e68a8ddc4f 

```bash
(venv) $ signing-tool -n test.ini product getall
{
    "count": 1,
    "items": [
        {
            "description": "Device certificate for Initial Device certificates",
            "id": "c57c5925-9a5c-4c57-a70a-74e68a8ddc4f",
            "name": "Dummy Device CA issuer",
            "state": 16
        }
    ],
    "next": "/products/?page=1",
    "pages": 1,
    "prev": "/products/?page=1"
}

(venv) $ signing-tool -n test.ini product \
    get -I c57c5925-9a5c-4c57-a70a-74e68a8ddc4f 
{
    "ca_info": [
        {
            "cert_override_payload": null,
            "certificate_type": "ROOT",
            "cn": "Dummy CA",
            "crl_distribution_points": null,
            "crl_expiry": null,
            "crl_issue_interval": null,
            "csr": null,
            "description": "Device Identity CA for Dummy Device CA issuer",
            "external_request_id": null,
            "external_root": null,
            "id": "017ba6a9-38f1-4243-adc1-640524095794",
            "issuer_ca_id": null,
            "key_override_id": null,
            "key_type": "ECDSAP256",
            "leafs": [],
            "originating_id": null,
            "policy_identifiers": null,
            "product_id": null,
            "profile_id": "a80ce30e-d992-4907-90b8-b3da744c89ca",
            "state": 16,
            "use_case": "DeviceCA"
        }
    ],
    "description": "Device certificate for Initial Device certificates",
    "enabled": null,
    "external_request_id": "e637820b-7e02-07bc-d067-1e18fcc70d61",
    "id": "c57c5925-9a5c-4c57-a70a-74e68a8ddc4f",
    "name": "Dummy Device CA issuer",
    "product_config_items": [],
    "product_operations": [
        {
            "approval_rule": {
                "allowed_groups": [
                    "b716abb1-2e3b-47a9-bca5-a3669faa50a6"
                ],
                "approval_groups": [
                    ""
                ],
                "blanket_groups": [
                    ""
                ],
                "description": "desc",
                "name": "This is rule"
            },
            "ca_use_case": null,
            "description": "Device certificates",
            "id": "d179e0d0-06b2-4ca4-876f-31c415107337",
            "name": "Issue device certificates",
            "operation_type": "IssueDeviceCertificate",
            "profile_id": "f1d0e083-1cf1-41da-8284-545f57550764",
            "token": null
        }
    ],
    "product_type": "Production",
    "rnd_keys": [],
    "state": 16
}
```

## Issuing Device certificates

### Create CSR

In real life scenario the device creates the key and the corresponding CSR.

```bash
(venv) $ openssl ecparam -name prime256v1 -genkey -noout -conv_form uncompressed -out /tmp/ec.key
(venv) $ openssl req -new -nodes -key /tmp/ec.key -outform DER -subj "/C=FI/O=Customer/CN=device-123" 2>/dev/null > /tmp/csr.bin
```

### Send the CSR for issuance

```bash
(venv) $ signing-tool -n test.ini production add -P c57c5925-9a5c-4c57-a70a-74e68a8ddc4f -C /tmp/csr.bin
{
    "certificate": null,
    "chain": [],
    "error_status": null,
    "id": "dd8c431e-0860-4001-878d-5bf7257d65ef",
    "state": 1,
    "subject": "CN=device-123,O=Customer,C=FI"
}
Certificate signing request sent. Request ID: dd8c431e-0860-4001-878d-5bf7257d65ef state: Created


(venv) $ signing-tool -n test.ini production \
    get -I dd8c431e-0860-4001-878d-5bf7257d65ef -O /tmp/crt
{
    "certificate": "LS0tLS1CRUdJ…(1,088 base64 chars elided; PEM certificate: CN=device-123,O=Laavat,C=FI; issuer CN=Dummy CA,O=Laavat,C=FI; EC secp256r1; valid 2025-06-16 to 9999-12-31)",
    "chain": [
        "LS0tLS1CRUdJ…(904 base64 chars elided; PEM certificate: CN=Dummy CA,O=Laavat,C=FI; issuer CN=Dummy CA,O=Laavat,C=FI; EC secp256r1; valid 2025-06-16 to 9999-12-31)"
    ],
    "error_status": null,
    "id": "dd8c431e-0860-4001-878d-5bf7257d65ef",
    "state": 16,
    "subject": "CN=device-123,O=Customer,C=FI"
}
Certificate signing request. Request ID: dd8c431e-0860-4001-878d-5bf7257d65ef state: Ready
certificate written to: /tmp/crt.crt
certificate chain written to: /tmp/crt.chain
```
