Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs/examples #151

Merged
merged 25 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish-dry-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install mkdocs-material==9.5.40
- name: Build Dokka HTML
run: ./gradlew mkDocsBuild
2 changes: 1 addition & 1 deletion .github/workflows/publish-pages-only.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install mkdocs-material==9.5.40
- name: Build Dokka HTML
run: ./gradlew mkDocsBuild
- name: Setup Pages
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install mkdocs-material==9.5.40
- name: Build Dokka HTML
run: ./gradlew mkDocsBuild
- name: Setup Pages
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ Better safe than sorry!
* Base OIDs on BigInteger instead of UInt
* Directly support UUID-based OID creation
* Implement hash-to-curve and hash-to-scalar as per RFC9380
* `JwsSignes.plainsignatureInput` is now a raw ByteArray
* `JwsSigned.prepareSignatureInput` now returns a raw ByteArray
* Use kotlinx-io as primary source for parsing
* Base number encoding/decoding on koltinx-io
* Base number encoding/decoding on kotlinx-io
* Remove parsing from iterator
* Base ASN.1 encoding and decoding on kotlinx-io
* Remove single element decoding from Iterator
* Introduce `prepareDigestInput()` to `IosHomebrewAttestation`
* Remove Legacy iOS Attestation

### 3.9.0 (Supreme 0.4.0)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The very first bit means that you can verify signatures on the JVM, Android and

### Do check out the full manual with examples and API docs [here](https://a-sit-plus.github.io/signum/)!
This README provides just an overview.
The full manual is more comprehensive, has separate sections for each module, and provides a full API documentation.
The full manual is more comprehensive, has separate sections for each module, provides examples, and a full API documentation.

## Using it in your Projects

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ internal fun App() {
is SignatureAlgorithm.ECDSA -> {
this@createSigningKey.ec {
curve = alg.requiredCurve ?:
ECCurve.entries.find { it.nativeDigest == alg.digest }!!
ECCurve.entries.find { it.nativeDigest == alg.digest }!!
digests = setOf(alg.digest)
}
}
Expand Down
317 changes: 317 additions & 0 deletions docs/docs/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# Signum Examples

This page demonstrates how to accomplish common tasks using _Signum_.

## Creating a Signed JSON Web Signature Object (`JwsSigned`)

!!! info inline end
This example requires the _Supreme_ KMP crypto provider and _Indispensable Josef_.

In this example, we'll start with an ephemeral P-256 signer:

```kotlin
val signer = Signer.Ephemeral {
ec { curve = ECCurve.SECP_256_R_1 }
}.getOrThrow() //TODO handle error
```

Next up, we'll create a header and payload:

```kotlin
val header = JwsHeader(
algorithm = signer.signatureAlgorithm.toJwsAlgorithm().getOrThrow(),
jsonWebKey = signer.publicKey.toJsonWebKey()
)
val payload = byteArrayOf(1, 3, 3, 7)
```

Since both header and payload are fed into the signature, we need to prepare this signature input:

```kotlin
val plainSignatureInput = JwsSigned.prepareJwsSignatureInput(header, payload)
```

Now, everything is ready to be signed:

```kotlin
val signature = signer.sign(plainSignatureInput).signature //TODO: handle error
JwsSigned(header, payload, signature, plainSignatureInput).serialize() // this we can verify on jwt.io
```

As can be seen, a `JwsSigned` takes header, payload, signature, and the plain signature input as parameters.
The reason for keeping this fourth parameter is convenience and efficiency: For one, you need this input to serialize a
`JwsSigned`, so it would be a waste to discard it. After parsing a `JswSigned` from its serialized form, you also need the
`plainSignatureInput` to verify everything was signed correctly.


## Creating a `CoseSigned` Object

!!! info inline end
This example requires the _Supreme_ KMP crypto provider and _Indispensable Cosef_.

In this example, we'll again start with an ephemeral P-256 signer:

```kotlin
val signer = Signer.Ephemeral {
ec { curve = ECCurve.SECP_256_R_1 }
}.getOrThrow() //TODO handle error properly
```

Next up, we'll create a header and payload:

```kotlin
//set KID + algorithm
val protectedHeader = CoseHeader(
algorithm = signer.signatureAlgorithm.toCoseAlgorithm().getOrElse { TODO() },
kid = signer.publicKey.didEncoded.encodeToByteArray()
)

val payload = byteArrayOf(0xC, 0xA, 0xF, 0xE)
```

Both of these are signature inputs, so we'll construct a `CoseSignatureInput` to sign.

```kotlin
val signatureInput = CoseSignatureInput(
contextString = "Signature1",
protectedHeader = ByteStringWrapper(protectedHeader),
externalAad = byteArrayOf(),
payload = payload,
).serialize()
```


Now, everything is ready to be signed:

```kotlin
val signature = signer.sign(signatureInput).signature //TODO handle error

val coseSigned = CoseSigned(
ByteStringWrapper(protectedHeader),
unprotectedHeader = null,
payload,
signature
).serialize() // sadly, there's no cwt.io, but you can use cbor.me to explore the signed data
```

## Create and Parse a Custom-Tagged ASN.1 Structure

!!! info inline end
This example requires only the _Indispensable_ module.

This example illustrates how to encapsulate a custom ASN.1 encoding scheme to make it reusable and composable.

### Definitions

Let's say you are using ASN.1 as your wire format for interoperability with different frameworks and languages.
This particular example demonstrates how log messages, i.e. the status of an operation, maybe from a smartcard, are sent off-device.

!!! note inline end
Such constraints may seem artificial, but when bandwidth is low, a compact representation is key.

A log message is an implicitly tagged ASN.1 structure with APPLICATION tag `26` and sequence semantics.
It contains the number of times an operation was run, and a timestamp, which can be either relative (in whole seconds since the last operation)
or absolute (UTC Time).
This relative/absolute flag uses the implicit APPLICATION tag `42` and the tuple of flag and time
is encoded into an ANS.1 OCTET STRING. This allows for two possible encodings, as illustrated below:

<table>
<tr>
<th>
Absolute Time
</th>
<th>
Relative Time
</th>
</tr>
<tr>
<td>

```asn1
Application 26 (2 elem)
INTEGER 1
OCTET STRING (19 byte)
Application 42 (1 byte) 00
UTCTime 2024-09-30 18:11:59 UTC
```

</td>

<td>

```asn1
Application 26 (2 elem)
INTEGER 3
OCTET STRING (7 byte)
Application 42 (1 byte) FF
INTEGER 39
```

</td>
</tr>
</table>

### Encoding

We'll be assuming absolute time to keep things simple.
Hence, the structure containing an absolute time can be created using the _Indispensable_ ASN.1 engine as follows:

```kotlin
val TAG_TIME_RELATIVE = 42uL withClass TagClass.APPLICATION

Asn1.Sequence {
+Asn1.Int(1)
+OctetStringEncapsulating {
+(Bool(false) withImplicitTag TAG_TIME_RELATIVE)
+Asn1Time(Clock.System.now())
}
} withImplicitTag (26uL withClass TagClass.APPLICATION)
// ↑ in reality this would be a constant ↑
```

The HEX-equivalent of this structure (which can be obtained by calling `.toDerHexString()`) is
[7F8A391802010104135F2A0100170D3234303933303138313135395A](https://lapo.it/asn1js/#f4o5GAIBAQQTXyoBABcNMjQwOTMwMTgxMTU5Wg).

### Parsing and Validating Tags

Basic parsing is straight-forward: You have DER-encoded bytes, and feed them into `AsnElement.parse()`.
In this example, you examine the first child to get the number of times the operation was carried out;
then, you decode the first child of the OCTET STRING that follows to decide how to decode the second child.

Usually, though (and especially when using implicit tags), you really want to verify those tags too.
Hence, parsing and properly validating is a bit more elaborate:

```kotlin linenums="1"
Asn1Element.parse(customSequence.derEncoded).asStructure().let { root ->

//↓↓↓ In reality, this would be a global constant; the same as in the previous snippet ↓↓↓
val rootTag = Asn1Element.Tag(26uL, tagClass = TagClass.APPLICATION, constructed = true)
root.assertTag(rootTag) //throws on tag mismatch

val numberOfOps = root.nextChild().asPrimitive().decodeToUInt()
root.nextChild().asEncapsulatingOctetString().let { timestamp ->
val isRelative = timestamp.nextChild().asPrimitive()
.decodeToBoolean(TAG_TIME_RELATIVE)

val time = if (isRelative) timestamp.nextChild().asPrimitive().decodeToUInt()
else timestamp.nextChild().asPrimitive().decodeToInstant()

if (timestamp.hasMoreChildren() || root.hasMoreChildren())
throw Asn1StructuralException("Superfluous Content")

// Everything is parsed and validated
TODO("Create domain object from $numberOfOps, $isRelative, and $time")
}
}
```

The above snippet performs the following validations:

1. Line 5 asserts the tag of the root structure
2. Line 7 ensures that the first child is an ASN.1 primitive tagged as INT, containing an unsigned integer
3. Line 8 execution successfully guarantees that the second child is indeed an ASN.1 OCTET STRING encapsulating
another ASN.1 structure.
4. Lines 9-10 verify that the first child contained in the ASN.1 OCTET STRING
* is an ASN.1 primitive
* tagged with `TAG_TIME_RELATIVE`
* containing an ASN.1 boolean
5. Line 12 ensures that the next child is an ASN.1 primitive, encoding an unsigned integer (in case an `UInt` is expected)
6. Line 13 tackles the alternative and ensures that the next child contains a properly encoded ASN.1 time
7. Lastly, lines 15-16 make sure no additional content is present, thus fully verifying the structure as a whole.


## Issuing Binding Certificates

!!! info inline end
This example requires the _Supreme_ KMP crypto provider. Only _Signum_-specifics are illustrated using code snippets.

We'll assume a JVM backend using _[WARDEN](https://github.com/a-sit-plus/warden)_ and trust anchors all set up correctly
on the client apps.
A common pattern in a mobile client setting in the context of banking or eID are so-called _binding certificates_ (or binding keys, but we'll stick to certificates here).
Just assume a bank with a mobile client application: Customers are typically issued an activation token out-of-band
(via mail, by the teller, …). This token is used to activate the app and transactions can then be authorized using biometrics.

In settings as critical as eID and banking, the service operator typically wants to ensure that only uncompromised clients
may access a service. To ensure this, the example described here relies on attestation.

This process works more or less as follows:

1. The client contacts the back-end to start the binding process
2. The back-end authenticates the binding request, identifying the customer. This could be a traditional authentication process, some out-of-band personalized token, etc.
3. The user enters this information into the client app and the app transmits this information to the back-end.
4. The back-end sends a challenge to the client
5. The client creates a new public-private key pair, using the challenge to also attest app, key, and the biometric authorization requirement (see [Attestation](supreme.md#attestation)).
```kotlin
val signer = PlatformSigningProvider.createSigningKey("binding") {
ec { curve = ECCurve.SECP_256_R_1 }
hardware {
backing = REQUIRED
attestation { challenge = challengeFromServer }
protection {
factors { biometry = true }
}
}
}.getOrElse { TODO("Handle error") }
```
6. The client creates and signs a CSR for the key, which includes the challenge and an attestation proof
```kotlin
val tbsCSR = TbsCertificationRequest(
subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))),
publicKey = signer.publicKey,
attributes = listOf(
Pkcs10CertificationRequestAttribute(
// No OID is assigned for this; choose one!
attestationOid,
// ↓↓↓ contains challenge ↓↓↓
Asn1String.UTF8(signer.attestation!!.jsonEncoded).encodeToTlv()
)
)
)

//extension function producing a signed CSR
val csr = signer.sign(tbsCSR).getOrElse { TODO("handle error") }
```
7. The back-end verifies the signature of the CSR, and validates the challenge and attestation information
```kotlin
X509SignatureAlgorithm.ES256.verifierFor(csr.tbsCsr.publicKey)
.getOrElse { TODO("Handle error") }
.verify(
csr.tbsCsr.encodeToDer(),
CryptoSignature.decodeFromDer(csr.signature)
).getOrElse { TODO("Abort here!") }

val attestation =
csr.tbsCsr.attributes.firstOrNull { it.oid == attestationOid }
?.value?.first() ?: TODO("Abort here!")
//TODO: feed attestation to WARDEN for verification
```
8. The back-end issues and signs a _binding certificate_ for the CSR, and transmits it to the client.
```kotlin
val tbsCrt = TbsCertificate(
serialNumber = Random.nextBytes(16),
signatureAlgorithm = signer.signatureAlgorithm.toX509SignatureAlgorithm().getOrThrow(),
issuerName = backendIssuerName,
validFrom = Asn1Time(Clock.System.now()),
validUntil = Asn1Time(Clock.System.now() + VALIDITY),
subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))),
publicKey = csr.tbsCsr.publicKey, //client public key
extensions = listOf(
// we want to indicate, that this client passed attestation checks
X509CertificateExtension(
attestedClientOid,
critical = true,
Asn1PrimitiveOctetString(byteArrayOf())
)
)
)

val clientCertificate = signer.sign(tbsCrt).getOrElse { TODO("handle error") }
```
9. The client stores the certificate.

To recap: This example shows how to
* instantiate a signer for a hardware-backed, biometry-protected, attested key
* instantiate a verifier
* create, sign and verify CSRs with a custom attribute
* extract a custom attribute from a CSR
* create, and sign a certificate with a custom critical extension
2 changes: 1 addition & 1 deletion docs/docs/indispensable.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ The base decoding function is called `decode()` and has the following signature:
fun <reified T> Asn1Primitive.decode(assertTag: ULong, transform: (content: ByteArray) -> T): T
```
An alternative exists, taking a `Tag` instead of an `Ulong`. in both cases a tag to assert and a user-defined transformation function is expected, which operates on
the content of the ASN.1 primitive. Moreover, npn-throwing `decodeOrNull` variant is present.
the content of the ASN.1 primitive. Moreover, non-throwing `decodeOrNull` variant is present.
In addition, the following self-describing shorthands are defined:

* `Asn1Primitive.decodeToBoolean()` throws on error
Expand Down
Loading
Loading