-
Notifications
You must be signed in to change notification settings - Fork 14
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
Introduce CCA Realm provisioning plugin #222
Changes from 1 commit
d2de33e
e4afbd1
2abfea9
0c71082
12644ad
86c4206
b7e1e5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
|
||
# Copyright 2024 Contributors to the Veraison project. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
.DEFAULT_GOAL := test | ||
|
||
GOPKG := github.com/veraison/services/scheme/cca-realm-provisioning | ||
SRCS := $(wildcard *.go) | ||
|
||
SUBDIR += plugin | ||
|
||
include ../../mk/common.mk | ||
include ../../mk/lint.mk | ||
include ../../mk/pkg.mk | ||
include ../../mk/subdir.mk | ||
include ../../mk/test.mk |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Endorsement Store Interface | ||
|
||
## Reference Value | ||
|
||
In CCA_Realm scheme the Realm instance is uniquely identified by the values of Realm initial measurements, | ||
used to launch a Realm. | ||
|
||
```json | ||
{ | ||
"scheme": "CCA_REALM", | ||
"type": "REFERENCE_VALUE", | ||
"attributes": { | ||
"CCA_REALM.vendor": "Worload Client Ltd", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/worload/workload/ |
||
"CCA_REALM.class-id": "CD1F0E55-26F9-460D-B9D8-F7FDE171787C", | ||
"CCA_REALM.inst-id": "QoS1aUymwNLPR4mguVrIAlyBjeUjBDZL580pgbLS7caFsyInfsJYGZYkE9jJssH1", | ||
"CCA_REALM.hash-alg-id": "sha-384", | ||
"CCA_REALM.measurements": [ | ||
{ | ||
"rim": "QoS1aUymwNLPR4mguVrIAlyBjeUjBDZL580pgbLS7caFsyInfsJYGZYkE9jJssH1" | ||
}, | ||
{ | ||
"rem0": "IQe752H8pS2VE2oTVNt6TdV7Gya+DT2nHZ6yOYazS6YVq/ZRTPNeWp6lWgMtBop4" | ||
}, | ||
{ | ||
"rem1": "JQe752H8pS2VE2oTVNt6TdV7Gya+DT2nHZ6yOYazS6YVq/ZRTPNeWp6lWgMtBop4" | ||
}, | ||
{ | ||
"rem2": "MQe752H8pS2VE2oTVNt6TdV7Gya+DT2nHZ6yOYazS6YVq/ZRTPNeWp6lWgMtBop4" | ||
}, | ||
{ | ||
"rem3": "NQe752H8pS2VE2oTVNt6TdV7Gya+DT2nHZ6yOYazS6YVq/ZRTPNeWp6lWgMtBop4" | ||
} | ||
] | ||
} | ||
} | ||
``` | ||
|
||
## Trust Anchor | ||
|
||
CCA_Realm scheme has no explicit Trust Anchor to provision, as they are supplied inline in the Realm attestation token |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// Copyright 2022-2024 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package cca_realm_provisioning | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/veraison/corim/comid" | ||
"github.com/veraison/services/log" | ||
) | ||
|
||
type ClassAttributes struct { | ||
UUID string | ||
Vendor string | ||
} | ||
|
||
// extract class variables from environment | ||
func (o *ClassAttributes) FromEnvironment(e comid.Environment) error { | ||
class := e.Class | ||
|
||
if class == nil { | ||
log.Debug("no class in the environment") | ||
return nil | ||
} | ||
|
||
classID := class.ClassID | ||
|
||
if classID == nil { | ||
log.Debug("no classID in the environment") | ||
} | ||
if classID != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be an |
||
uuID, err := classID.GetUUID() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: rename to |
||
if err != nil { | ||
return fmt.Errorf("could not extract uu-id from class-id: %w", err) | ||
} | ||
|
||
if err := uuID.Valid(); err != nil { | ||
return fmt.Errorf("no valid uu-id: %w", err) | ||
} | ||
|
||
o.UUID = uuID.String() | ||
} | ||
|
||
if class.Vendor != nil { | ||
o.Vendor = *class.Vendor | ||
} else { | ||
return errors.New("class is neither UUID or Vendor Name") | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright 2024 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package cca_realm_provisioning | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/veraison/corim/comid" | ||
"github.com/veraison/services/handler" | ||
) | ||
|
||
type CorimExtractor struct{} | ||
|
||
func (o CorimExtractor) RefValExtractor( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename to (note: I realise that this is meant to be implementing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IExtractor is the interface which is been implemented by CorimExtractor. To your comment: however there is no need for that interface -- this is only used within this code is not true, |
||
rv comid.ReferenceValue, | ||
) ([]*handler.Endorsement, error) { | ||
var classAttrs ClassAttributes | ||
var instAttrs InstanceAttributes | ||
|
||
if err := classAttrs.FromEnvironment(rv.Environment); err != nil { | ||
return nil, fmt.Errorf("could not extract Realm class attributes: %w", err) | ||
} | ||
|
||
if err := instAttrs.FromEnvironment(rv.Environment); err != nil { | ||
return nil, fmt.Errorf("could not extract Realm instance attributes: %w", err) | ||
} | ||
|
||
// Each measurement is encoded in a measurement-map of a CoMID | ||
// reference-triple-record. Since a measurement-map can encode one or more | ||
// measurements, a single reference-triple-record can carry as many | ||
// measurements as needed. However for Realm Instance, only one measurement | ||
// record is set, with both the "rim" & "rem" measurements carried in an | ||
// integrity register | ||
yogeshbdeshpande marked this conversation as resolved.
Show resolved
Hide resolved
|
||
refVals := make([]*handler.Endorsement, 0, len(rv.Measurements)) | ||
|
||
var refVal *handler.Endorsement | ||
for _, m := range rv.Measurements { | ||
var rAttr RealmAttributes | ||
if err := rAttr.FromMeasurement(m); err != nil { | ||
return nil, fmt.Errorf("unable to extract realm reference attributes from measurement: %w", err) | ||
} | ||
refAttrs, err := makeRefValAttrs(&classAttrs, &instAttrs, &rAttr) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to make reference attributes: %w", err) | ||
} | ||
refVal = &handler.Endorsement{ | ||
Scheme: "CCA_REALM", | ||
Type: handler.EndorsementType_REFERENCE_VALUE, | ||
Attributes: refAttrs, | ||
} | ||
refVals = append(refVals, refVal) | ||
} | ||
|
||
if len(refVals) == 0 { | ||
return nil, fmt.Errorf("no measurements found") | ||
} | ||
|
||
return refVals, nil | ||
} | ||
|
||
func makeRefValAttrs(cAttr *ClassAttributes, | ||
iAttr *InstanceAttributes, | ||
rAttr *RealmAttributes) (json.RawMessage, error) { | ||
|
||
var attrs = map[string]interface{}{ | ||
"CCA_REALM.vendor": cAttr.Vendor, | ||
"CCA_REALM-class-id": cAttr.UUID, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it the right choice? Would "model" be a better identifier for what is essentially a software product? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
"CCA_REALM-instance-id": rAttr.Rim, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be "CCA_REALM-realm-initial-measurement" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it can be expressed this way and perhaps better, hence renamed it now! |
||
"CCA_REALM.hash-alg-id": rAttr.HashAlgID, | ||
"CCA_REALM.rim": rAttr.Rim, | ||
"CCA_REALM.rem0": rAttr.Rem[0], | ||
"CCA_REALM.rem1": rAttr.Rem[1], | ||
"CCA_REALM.rem2": rAttr.Rem[2], | ||
"CCA_REALM.rem3": rAttr.Rem[3], | ||
} | ||
|
||
data, err := json.Marshal(attrs) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to marshal reference value attributes: %w", err) | ||
} | ||
return data, nil | ||
} | ||
|
||
func (o CorimExtractor) TaExtractor( | ||
yogeshbdeshpande marked this conversation as resolved.
Show resolved
Hide resolved
|
||
avk comid.AttestVerifKey, | ||
) (*handler.Endorsement, error) { | ||
return nil, fmt.Errorf("cca realm endorsements does not have a Trust Anchor") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright 2022-2024 Contributors to the Veraison project. | ||
yogeshbdeshpande marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package cca_realm_provisioning | ||
|
||
// automatically generated from: | ||
// comidCcaRealm.json and corimCcaRealm.json | ||
var unsignedCorimcomidCcaRealm = ` | ||
a500505c57e8f446cd421b91c908cf93e13cfc01815901eed901faa40065 | ||
656e2d474201a1005043bbe37f2e614b33aed353cff1428b160281a30074 | ||
576f726b6c6f616420436c69656e74204c74642e01d820781e6874747073 | ||
3a2f2f776f726b6c6f6164636c69656e742e6578616d706c650283000102 | ||
04a1008182a200a200d82550cd1f0e5526f9460db9d8f7fde171787c0173 | ||
576f726b6c6f616420436c69656e74204c746401d9023058304284b5694c | ||
a6c0d2cf4789a0b95ac8025c818de52304364be7cd2981b2d2edc685b322 | ||
277ec25819962413d8c9b2c1f581a101a10ea56372696d81820758304284 | ||
b5694ca6c0d2cf4789a0b95ac8025c818de52304364be7cd2981b2d2edc6 | ||
85b322277ec25819962413d8c9b2c1f56472656d3081820758302107bbe7 | ||
61fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615ab | ||
f6514cf35e5a9ea55a032d068a786472656d3181820758302507bbe761fc | ||
a52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615abf651 | ||
4cf35e5a9ea55a032d068a786472656d3281820758303107bbe761fca52d | ||
95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615abf6514cf3 | ||
5e5a9ea55a032d068a786472656d3381820758303507bbe761fca52d9513 | ||
6a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615abf6514cf35e5a | ||
9ea55a032d068a780381781a687474703a2f2f61726d2e636f6d2f636361 | ||
2f7265616c6d2f3104a200c11a61ce480001c11a695467800581a3006941 | ||
434d45204c74642e01d8206c61636d652e6578616d706c65028101 | ||
` | ||
|
||
// automatically generated from: | ||
// comidCcaRealmNoClass.json and corimCcaRealm.json | ||
var unsignedCorimcomidCcaRealmNoClass = ` | ||
a500505c57e8f446cd421b91c908cf93e13cfc01815901c3d901faa40065 | ||
656e2d474201a1005043bbe37f2e614b33aed353cff1428b160281a30074 | ||
576f726b6c6f616420436c69656e74204c74642e01d820781e6874747073 | ||
3a2f2f776f726b6c6f6164636c69656e742e6578616d706c650283000102 | ||
04a1008182a101d9023058304284b5694ca6c0d2cf4789a0b95ac8025c81 | ||
8de52304364be7cd2981b2d2edc685b322277ec25819962413d8c9b2c1f5 | ||
81a101a10ea56372696d81820758304284b5694ca6c0d2cf4789a0b95ac8 | ||
025c818de52304364be7cd2981b2d2edc685b322277ec25819962413d8c9 | ||
b2c1f56472656d3081820758302107bbe761fca52d95136a1354db7a4dd5 | ||
7b1b26be0d3da71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a | ||
786472656d3181820758302507bbe761fca52d95136a1354db7a4dd57b1b | ||
26be0d3da71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a7864 | ||
72656d3281820758303107bbe761fca52d95136a1354db7a4dd57b1b26be | ||
0d3da71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a78647265 | ||
6d3381820758303507bbe761fca52d95136a1354db7a4dd57b1b26be0d3d | ||
a71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a780381781a68 | ||
7474703a2f2f61726d2e636f6d2f6363612f7265616c6d2f3104a200c11a | ||
61ce480001c11a695467800581a3006941434d45204c74642e01d8206c61 | ||
636d652e6578616d706c65028101 | ||
` | ||
|
||
// automatically generated from: | ||
// comidCcaRealmNoInstance.json and corimCcaRealm.json | ||
var unsignedCorimcomidCcaRealmNoInstance = ` | ||
a500505c57e8f446cd421b91c908cf93e13cfc01815901b8d901faa40065 | ||
656e2d474201a1005043bbe37f2e614b33aed353cff1428b160281a30074 | ||
576f726b6c6f616420436c69656e74204c74642e01d820781e6874747073 | ||
3a2f2f776f726b6c6f6164636c69656e742e6578616d706c650283000102 | ||
04a1008182a100a200d82550cd1f0e5526f9460db9d8f7fde171787c0173 | ||
576f726b6c6f616420436c69656e74204c746481a101a10ea56372696d81 | ||
820758304284b5694ca6c0d2cf4789a0b95ac8025c818de52304364be7cd | ||
2981b2d2edc685b322277ec25819962413d8c9b2c1f56472656d30818207 | ||
58302107bbe761fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb239 | ||
86b34ba615abf6514cf35e5a9ea55a032d068a786472656d318182075830 | ||
2507bbe761fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b3 | ||
4ba615abf6514cf35e5a9ea55a032d068a786472656d3281820758303107 | ||
bbe761fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba6 | ||
15abf6514cf35e5a9ea55a032d068a786472656d3381820758303507bbe7 | ||
61fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615ab | ||
f6514cf35e5a9ea55a032d068a780381781a687474703a2f2f61726d2e63 | ||
6f6d2f6363612f7265616c6d2f3104a200c11a61ce480001c11a69546780 | ||
0581a3006941434d45204c74642e01d8206c61636d652e6578616d706c65 | ||
028101 | ||
` | ||
|
||
// automatically generated from: | ||
// comidCcaRealmInvalidInstance.json and corimCcaRealm.json | ||
var unsignedCorimcomidCcaRealmInvalidInstance = ` | ||
a500505c57e8f446cd421b91c908cf93e13cfc01815901dfd901faa40065 | ||
656e2d474201a1005043bbe37f2e614b33aed353cff1428b160281a30074 | ||
576f726b6c6f616420436c69656e74204c74642e01d820781e6874747073 | ||
3a2f2f776f726b6c6f6164636c69656e742e6578616d706c650283000102 | ||
04a1008182a200a200d82550cd1f0e5526f9460db9d8f7fde171787c0173 | ||
576f726b6c6f616420436c69656e74204c746401d90226582101ceebae7b | ||
8927a3227e5303cf5e0f1f7b34bb542ad7250ac03fbcde36ec2f150881a1 | ||
01a10ea56372696d81820758304284b5694ca6c0d2cf4789a0b95ac8025c | ||
818de52304364be7cd2981b2d2edc685b322277ec25819962413d8c9b2c1 | ||
f56472656d3081820758302107bbe761fca52d95136a1354db7a4dd57b1b | ||
26be0d3da71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a7864 | ||
72656d3181820758302507bbe761fca52d95136a1354db7a4dd57b1b26be | ||
0d3da71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a78647265 | ||
6d3281820758303107bbe761fca52d95136a1354db7a4dd57b1b26be0d3d | ||
a71d9eb23986b34ba615abf6514cf35e5a9ea55a032d068a786472656d33 | ||
81820758303507bbe761fca52d95136a1354db7a4dd57b1b26be0d3da71d | ||
9eb23986b34ba615abf6514cf35e5a9ea55a032d068a780381781a687474 | ||
703a2f2f61726d2e636f6d2f6363612f7265616c6d2f3104a200c11a61ce | ||
480001c11a695467800581a3006941434d45204c74642e01d8206c61636d | ||
652e6578616d706c65028101 | ||
` | ||
|
||
// automatically generated from: | ||
// comidCcaRealmInvalidClass.json and corimCcaRealm.json | ||
var unsignedCorimcomidCcaRealmInvalidClass = ` | ||
a500505c57e8f446cd421b91c908cf93e13cfc01815901f1d901faa40065 | ||
656e2d474201a1005043bbe37f2e614b33aed353cff1428b160281a30074 | ||
576f726b6c6f616420436c69656e74204c74642e01d820781e6874747073 | ||
3a2f2f776f726b6c6f6164636c69656e742e6578616d706c650283000102 | ||
04a1008182a200a200d90258582061636d652d696d706c656d656e746174 | ||
696f6e2d69642d303030303030303031016441434d4501d9023058304284 | ||
b5694ca6c0d2cf4789a0b95ac8025c818de52304364be7cd2981b2d2edc6 | ||
85b322277ec25819962413d8c9b2c1f581a101a10ea56372696d81820758 | ||
304284b5694ca6c0d2cf4789a0b95ac8025c818de52304364be7cd2981b2 | ||
d2edc685b322277ec25819962413d8c9b2c1f56472656d30818207583021 | ||
07bbe761fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34b | ||
a615abf6514cf35e5a9ea55a032d068a786472656d3181820758302507bb | ||
e761fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615 | ||
abf6514cf35e5a9ea55a032d068a786472656d3281820758303107bbe761 | ||
fca52d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615abf6 | ||
514cf35e5a9ea55a032d068a786472656d3381820758303507bbe761fca5 | ||
2d95136a1354db7a4dd57b1b26be0d3da71d9eb23986b34ba615abf6514c | ||
f35e5a9ea55a032d068a780381781a687474703a2f2f61726d2e636f6d2f | ||
6363612f7265616c6d2f3104a200c11a61ce480001c11a695467800581a3 | ||
006941434d45204c74642e01d8206c61636d652e6578616d706c65028101 | ||
` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though this currently only implements the provisioning part, shouldn't the scheme be just
cca-realm
(and any future verification implementation would go under here as well)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@setrofim : As discussed only the
provisioning
will be a separate scheme, as it is coming from a different supply chain, the Verification will be taken care inside a combined CCA plugin, which verifies both Realm and Platform part together. Hence I chosen the name cca-realm-provisioning.If I keep it just
cca-realm
then it will assume both Provisioning and Verifications are handled inside!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm ok, that would mean that the
StoreHandler
implementation would essentially be split/duplicated across two schemes, as the key formats need to be aligned across both schemes (which breaks the conceptual integrity of a scheme -- schems ought to be independent of each other).Are we sure this is the right way forward? Would it not be better to implement as single
cca
scheme that handles everything in a single set of plugins, and then use configuration and/or policy to control whether the platform or realm parts, or both, are handled by a particular deployment?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I thought about StoreHandler part, while implementing the provisioning scheme and came to conclusion that the internal
logic
will be implemented in thecommon
set of functions and both the schemes will invoke the same internal implementation thus essentially avoiding duplication! This is the best way of approaching as the CCA Scheme StoreHandler needs to fetch in sequence the RefValues for Platform and Realm in sequence, however this scheme will only do that latter!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conceptually, a "scheme" is supposed to encompass the entire attestation "lifecycle", i.e. provisioning and attestion. It doesn't make sense to have a scheme with only provisioning, after all, the sole purpose of provisioning endorsements is so that they would later be used in verification.
If that is the case, then this should be part of that combined plugin as well (going back to my intial suggestion of renaming this). When implementing the verification part of it, we can then re-use the commmon functionality between this and the
cca-ssd-platform
scheme.On the other hand, if the goal is to modify
cca-ssd-platfom
scheme to optinally handle the realm part as well, then (it should probably be renamed, and) this should be part of that too.Basically, it doesn't make sense for provisioning to exist in a scheme of its own, as the only purpose of provisioning is to enable verification. So if realm verification on its own is not a thing, and we can only have plaform and platform+realm, then we should have two schemes:
cca-ssd-platform
andcca
(orcca-combined
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not incorrect, as long as the same CoRIM profile takes care of both platform and realm.
Instead, if we want to have separate profiles, we will need two media types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, they should be separate media types/profiles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, CCA Platform and CCA Realm are two separate profiles and hence we will need two media types!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They technically could be handled with a single profile. However, I belive the arguement is that they should be handled by separate profiles, as they're expected to be privided by different entitied within the supply chain, and that would simplify the profile that each entity needs to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are clear practical advantages to separating the two. I was just noting that it's not "incorrect" per se.
I think we are all in agreement about "two media types for one scheme"